Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
variable_expand.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 <cstdint>
17#include <cstdlib>
18#include <limits>
19#include <optional>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/algorithm/container.h"
25#include "absl/base/log_severity.h"
26#include "absl/container/btree_map.h"
27#include "absl/log/check.h"
28#include "absl/log/log.h"
29#include "absl/strings/str_cat.h"
36
37namespace operations_research {
38namespace sat {
39
40namespace {
41enum class EncodingLinear1Type {
42 kVarEqValue = 0,
43 kVarNeValue,
44 kVarGeValue,
45 kVarLeValue,
46 kVarInDomain,
47};
48const int kNumEncodingLinear1Types = 5;
49
50template <typename Sink>
51void AbslStringify(Sink& sink, EncodingLinear1Type type) {
52 switch (type) {
53 case EncodingLinear1Type::kVarEqValue:
54 sink.Append("kVarEqValue");
55 return;
56 case EncodingLinear1Type::kVarNeValue:
57 sink.Append("kVarNeValue");
58 return;
59 case EncodingLinear1Type::kVarGeValue:
60 sink.Append("kVarGeValue");
61 return;
62 case EncodingLinear1Type::kVarLeValue:
63 sink.Append("kVarLeValue");
64 return;
65 case EncodingLinear1Type::kVarInDomain:
66 sink.Append("kVarInDomain");
67 return;
68 }
69}
70
71enum class EncodingLinear1Status {
72 kOk = 0,
73 kIgnore,
74 kUnsat,
75 kAbort,
76};
77
78struct EncodingLinear1 {
79 EncodingLinear1Type type;
80 int64_t value = std::numeric_limits<int64_t>::min();
81 Domain rhs; // Only used for kVarInDomain.
82 int enforcement_literal;
83 int constraint_index;
84
85 std::string ToString() const {
86 return absl::StrCat("EncodingLinear1(type: ", type, ", value: ", value,
87 ", rhs: ", rhs.ToString(),
88 ", enforcement_literal: ", enforcement_literal,
89 ", constraint_index: ", constraint_index, ")");
90 }
91};
92
93std::pair<std::optional<EncodingLinear1>, EncodingLinear1Status> ProcessLinear1(
94 PresolveContext* context, int constraint_index, const Domain& var_domain) {
95 const ConstraintProto& ct =
96 context->working_model->constraints(constraint_index);
97 const Domain rhs = ReadDomainFromProto(ct.linear())
98 .InverseMultiplicationBy(ct.linear().coeffs(0))
99 .IntersectionWith(var_domain);
100 EncodingLinear1 lin;
101 lin.enforcement_literal = ct.enforcement_literal(0);
102 lin.constraint_index = constraint_index;
103
104 if (rhs.IsEmpty()) {
105 if (!context->SetLiteralToFalse(lin.enforcement_literal)) {
106 return {std::nullopt, EncodingLinear1Status::kUnsat};
107 } else {
108 return {std::nullopt, EncodingLinear1Status::kIgnore};
109 }
110 } else if (rhs.IsFixed()) {
111 if (!var_domain.Contains(rhs.FixedValue())) {
112 if (!context->SetLiteralToFalse(lin.enforcement_literal)) {
113 return {std::nullopt, EncodingLinear1Status::kUnsat};
114 }
115 return {std::nullopt, EncodingLinear1Status::kIgnore};
116 } else {
117 lin.type = EncodingLinear1Type::kVarEqValue;
118 lin.value = rhs.FixedValue();
119 }
120 } else {
121 const Domain complement = var_domain.IntersectionWith(rhs.Complement());
122 if (complement.IsEmpty()) {
123 return {std::nullopt, EncodingLinear1Status::kIgnore};
124 } else if (complement.IsFixed()) {
125 DCHECK(var_domain.Contains(complement.FixedValue()));
126 lin.type = EncodingLinear1Type::kVarNeValue;
127 lin.value = complement.FixedValue();
128 } else if (rhs.Min() > complement.Max()) {
129 lin.type = EncodingLinear1Type::kVarGeValue;
130 lin.value = rhs.Min();
131 } else if (rhs.Max() < complement.Min()) {
132 lin.type = EncodingLinear1Type::kVarLeValue;
133 lin.value = rhs.Max();
134 } else {
135 lin.type = EncodingLinear1Type::kVarInDomain;
136 lin.rhs = rhs;
137 }
138 }
139 return {lin, EncodingLinear1Status::kOk};
140}
141
142template <typename Sink>
143void AbslStringify(Sink& sink, const EncodingLinear1& lin) {
144 sink.Append(lin.ToString());
145}
146
147} // namespace
148
150 : var_(var), var_domain_(context->DomainOf(var)), context_(context) {}
151
153 DCHECK(!is_closed_);
154 encoded_values_.push_back(value);
155 referenced_encoded_values_.push_back(value);
156}
157
159 DCHECK(!is_closed_);
160 encoded_values_.push_back(value);
161}
162
164 bool push_down_when_unconstrained, bool has_le_ge_linear1) {
165 if (!is_closed_) {
166 // Note: is_closed is true if we have forced the full encoding.
167 if (!has_le_ge_linear1) {
168 // We forget all optional encoded values as we just need one escape value.
169 encoded_values_ = referenced_encoded_values_;
170 }
171
172 gtl::STLSortAndRemoveDuplicates(&encoded_values_);
173 // Add an escape value to the existing encoded values when the encoding is
174 // not complete. The choice of the escape value depends on the presence of
175 // an objective and its direction. An encoded value not actually referenced
176 // by a linear1 constraint can be used as an escape value.
177 if (encoded_values_.size() < var_domain_.Size()) {
178 const Domain residual = var_domain_.IntersectionWith(
179 Domain::FromValues(referenced_encoded_values_).Complement());
180 const int64_t escape_value =
181 push_down_when_unconstrained ? residual.Min() : residual.Max();
182 // NOTE: escape_value can already be in all_encoded_values_.
183 encoded_values_.push_back(escape_value);
184 gtl::STLSortAndRemoveDuplicates(&encoded_values_);
185 if (!has_le_ge_linear1) {
186 unique_escape_value_ = escape_value;
187 }
188 }
189 is_closed_ = true;
190 is_fully_encoded_ = encoded_values_.size() == var_domain_.Size();
191 }
192}
193
194const std::vector<int64_t>& ValueEncoding::encoded_values() const {
195 DCHECK(is_closed_);
196 return encoded_values_;
197}
198
200 DCHECK(is_closed_);
201 return encoded_values_.empty();
202}
203
205 DCHECK(is_closed_);
206 return is_fully_encoded_;
207}
208
209std::optional<int64_t> ValueEncoding::unique_escape_value() const {
210 DCHECK(is_closed_);
211 return unique_escape_value_;
212}
213
215 encoded_values_.clear();
216 referenced_encoded_values_.clear();
217 encoded_values_.reserve(var_domain_.Size());
218 for (const int64_t v : var_domain_.Values()) {
219 encoded_values_.push_back(v);
220 }
221 is_closed_ = true;
222 is_fully_encoded_ = true;
223}
224
226 DCHECK(is_closed_);
227 for (const int64_t value : encoded_values_) {
228 encoding_[value] = context_->GetOrCreateVarValueEncoding(var_, value);
229 }
230}
231
232int ValueEncoding::literal(int64_t value) const {
233 DCHECK(is_closed_);
234 const auto it = encoding_.find(value);
235 DCHECK(it != encoding_.end());
236 return it->second;
237}
238
239const absl::btree_map<int64_t, int>& ValueEncoding::encoding() const {
240 DCHECK(is_closed_);
241 return encoding_;
242}
243
245 SolutionCrush& solution_crush)
246 : var_(var),
247 var_domain_(context->DomainOf(var)),
248 context_(context),
249 solution_crush_(solution_crush) {}
250
251void OrderEncoding::InsertLeLiteral(int64_t value, int literal) {
252 if (!tmp_le_to_literals_[value].insert(literal).second) return;
253 DCHECK_LT(value, var_domain_.Max());
254 const int64_t next_value = var_domain_.ValueAtOrAfter(value + 1);
255 if (tmp_ge_to_literals_[next_value].contains(NegatedRef(literal))) {
256 const auto [it, inserted] = encoded_le_literal_.insert({value, literal});
257 if (!inserted) {
258 VLOG(2) << "Duplicate var_le_value literal: " << literal
259 << " for value: " << value << " previous: " << it->second;
260 }
261 }
262}
263
264void OrderEncoding::InsertGeLiteral(int64_t value, int literal) {
265 if (!tmp_ge_to_literals_[value].insert(literal).second) return;
266 DCHECK_GT(value, var_domain_.Min());
267 const int64_t previous_value = var_domain_.ValueAtOrBefore(value - 1);
268 if (tmp_le_to_literals_[previous_value].contains(NegatedRef(literal))) {
269 const auto [it, inserted] =
270 encoded_le_literal_.insert({previous_value, NegatedRef(literal)});
271 if (!inserted) {
272 VLOG(2) << "Duplicate var_le_value literal: " << NegatedRef(literal)
273 << " for value: " << previous_value
274 << " previous: " << it->second;
275 }
276 }
277}
278
279// In the following examples, x has 5 values: 0, 1, 2, 3, 4, and some order
280// encoding literals.
281// 0 1 2 3 4
282// x_le_0 x_le_1 x_le_3
283// x_ge_1 x_ge_3 x_ge_4
284//
285// x_le_0 => not(x == 1) && x_le_1
286// x_le_1 => not(x == 2) && not(x == 3) && x_le_3
287//
288// x_ge_1 => not(x == 0)
289// x_ge_3 => not(x == 1) && not(x == 2) && x_ge_1
290// x_ge_4 => not(x == 3) && x_ge_3
291//
292// x_le_0 => x == 0
293// x_le_1 => x == 1 || x_le_0
294// x_le_3 => x == 3 || x == 2 || x_le_1
295//
296// x_ge_1 => x == 1 || x == 2 || x_ge_3
297// x_ge_3 => x == 3 || x == x_ge_4
298// x_ge_4 => x == 4
299//
300// If we have x_le_0 and x_ge_4, then we can infer x_le_4 and x_ge_0.
301// This is done by the code below.
303 const ValueEncoding& values) {
304 CollectAllOrderEncodingValues();
305 if (encoded_le_literal_.empty()) return;
306
307 if (DEBUG_MODE) {
308 // Check that all values are present in the encoding.
309 for (const auto& [value, literal] : encoded_le_literal_) {
310 CHECK(values.encoding().contains(value));
311 CHECK(values.encoding().contains(var_domain_.ValueAtOrAfter(value + 1)))
312 << "Cannot find " << var_domain_.ValueAtOrAfter(value + 1)
313 << " for var <= " << value;
314 }
315 }
316
317 const int64_t max_le_value = encoded_le_literal_.rbegin()->first;
318 const int64_t max_ge_value = var_domain_.ValueAtOrAfter(max_le_value + 1);
319 ConstraintProto* not_le = nullptr;
320 ConstraintProto* not_ge = context_->working_model->add_constraints();
321 ConstraintProto* le = context_->working_model->add_constraints();
322 ConstraintProto* ge = nullptr;
323
324 for (const auto [value, eq_literal] : values.encoding()) {
325 const int ne_literal = NegatedRef(eq_literal);
326
327 // Lower or equal.
328 if (not_le != nullptr) {
329 not_le->mutable_bool_and()->add_literals(ne_literal);
330 }
331 if (le != nullptr) {
332 le->mutable_bool_or()->add_literals(eq_literal);
333 }
334
335 const auto it_le = encoded_le_literal_.find(value);
336 if (it_le != encoded_le_literal_.end()) {
337 const int le_literal = it_le->second;
338
339 DCHECK(le != nullptr);
341 if (value < max_le_value) {
342 le = context_->working_model->add_constraints();
344 } else {
345 le = nullptr;
346 }
347
348 if (not_le != nullptr) {
350 }
351 not_le = context_->AddEnforcedConstraint({le_literal});
352 }
353
354 // Greater or equal.
355 if (value > var_domain_.Min()) { // var >= min is not created..
356 const auto it_ge =
357 encoded_le_literal_.find(var_domain_.ValueAtOrBefore(value - 1));
358 if (it_ge != encoded_le_literal_.end()) {
359 const int ge_literal = NegatedRef(it_ge->second);
360
361 if (ge != nullptr) {
363 }
364 ge = context_->AddEnforcedConstraint({ge_literal});
365
366 DCHECK(not_ge != nullptr);
368 if (value != max_ge_value) {
369 not_ge = context_->working_model->add_constraints();
371 } else {
372 not_ge = nullptr;
373 }
374 }
375 }
376 if (ge != nullptr) {
377 ge->mutable_bool_or()->add_literals(eq_literal);
378 }
379 if (not_ge != nullptr) {
380 not_ge->mutable_bool_and()->add_literals(ne_literal);
381 }
382 }
383}
384
385int OrderEncoding::ge_literal(int64_t value) const {
386 DCHECK_GT(value, var_domain_.Min());
387 const auto it =
388 encoded_le_literal_.find(var_domain_.ValueAtOrBefore(value - 1));
389 DCHECK(it != encoded_le_literal_.end());
390 return NegatedRef(it->second);
391}
392
393int OrderEncoding::le_literal(int64_t value) const {
394 const auto it = encoded_le_literal_.find(value);
395 DCHECK(it != encoded_le_literal_.end());
396 return it->second;
397}
398
400 return encoded_le_literal_.size();
401}
402
403void OrderEncoding::CollectAllOrderEncodingValues() {
404 for (const auto& [value, lits] : tmp_le_to_literals_) {
405 const auto it = encoded_le_literal_.find(value);
406 if (it != encoded_le_literal_.end()) continue;
407 const int le_literal = context_->NewBoolVar("order encoding");
408 solution_crush_.MaybeSetLiteralToOrderEncoding(le_literal, var_, value,
409 /*is_le=*/true);
410 encoded_le_literal_[value] = le_literal;
411 }
412
413 for (const auto& [value, lits] : tmp_ge_to_literals_) {
414 const int64_t previous_value = var_domain_.ValueAtOrBefore(value - 1);
415 const auto it = encoded_le_literal_.find(previous_value);
416 if (it != encoded_le_literal_.end()) continue;
417 const int ge_literal = context_->NewBoolVar("order encoding");
418 solution_crush_.MaybeSetLiteralToOrderEncoding(ge_literal, var_, value,
419 /*is_le=*/false);
420 encoded_le_literal_[previous_value] = NegatedRef(ge_literal);
421 }
422}
423
425 int var, PresolveContext* context, ValueEncoding& values,
426 OrderEncoding& order,
427 std::vector<std::vector<EncodingLinear1>>& linear_ones_by_type,
428 std::vector<int>& constraint_indices, bool& var_in_objective,
429 bool& var_has_positive_objective_coefficient) {
430 // We have a variable that appears only in linear1 constraints. That means
431 // that the model should not change feasibility as long as the values that
432 // the variable take does not the satisfiability of the linear1s. Thus, we
433 // have "domains of equivalence" of the possible values of the variable.
434 // Suppose we have a variable `v` in the domain `[0, 15]` that is only used in
435 // the following linear1:
436 // (c1) x => v <= 10
437 // (c2) y => v == 5
438 // (c3) z => 2 <= v <= 7
439 //
440 // We can represent that as intervals:
441 //
442 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
443 // +---------------------------------------------+
444 // v <= 10 ...............................*
445 // v == 5 *
446 // 2 <= v <= 7 *..............*
447 //
448 // If we cut the classes of equivalences for `v` into contiguous intervals,
449 // they are:
450 // - [0, 1] (c1, ~c2, ~c3)
451 // - [2, 4] (c1, ~c2, c3)
452 // - {5} (c1, c2, c3)
453 // - [6, 7] (c1, ~c2, c3)
454 // - [8, 10] (c1, ~c2, ~c3)
455 // - [11, 15] (~c1, ~c2, ~c3)
456 //
457 // We can pick either the lowest or the highest value of each interval
458 // depending on the sign of the objective coefficient. Thus, a possible
459 // encoding would be:
460 // - x == 0
461 // - x == 2
462 // - x == 5
463 // - x == 6
464 // - x == 8
465 // - x == 11
466 //
467 // TODO(user): Pick a single value for each equivalence class, not one
468 // per contiguous interval.
469 // TODO(user): Supports more domains, for now only <= and >= are supported.
470 //
471 // TODO(user): At the expense of a more complex hint manipulation, we could
472 // only use one optional value per variable.
473 const Domain& var_domain = context->DomainOf(var);
474 constraint_indices.clear();
475 var_in_objective = false;
476 var_has_positive_objective_coefficient = false;
477 for (const int c : context->VarToConstraints(var)) {
478 if (c == kObjectiveConstraint) {
479 const int64_t obj_coeff = context->ObjectiveMap().at(var);
480 var_in_objective = true;
481 var_has_positive_objective_coefficient = obj_coeff > 0;
482 continue;
483 }
484 if (c < 0) continue;
485 constraint_indices.push_back(c);
486 }
487
488 const bool push_down_when_unconstrained =
489 !var_in_objective || var_has_positive_objective_coefficient;
490
491 // Sort the constraint indices to make the encoding deterministic.
492 absl::c_sort(constraint_indices);
493 for (const int c : constraint_indices) {
494 const ConstraintProto& ct = context->working_model->constraints(c);
496 DCHECK_EQ(ct.linear().vars().size(), 1);
497 DCHECK(RefIsPositive(ct.linear().vars(0)));
498 DCHECK_EQ(ct.linear().vars(0), var);
499 if (ct.enforcement_literal().size() != 1) {
500 context->UpdateRuleStats(
501 "TODO variables: linear1 with multiple enforcement literals");
502 return false;
503 }
504
505 const auto& [opt_lin, status] = ProcessLinear1(context, c, var_domain);
506 if (!opt_lin.has_value()) {
507 if (status == EncodingLinear1Status::kUnsat) return false;
508 if (status == EncodingLinear1Status::kIgnore) continue;
509 if (status == EncodingLinear1Status::kAbort) {
510 context->UpdateRuleStats(
511 "TODO variables: only used in complex linear1");
512 return false;
513 }
514 }
515
516 const EncodingLinear1& lin = opt_lin.value();
517 VLOG(3) << "ProcessVariableOnlyUsedInEncoding(): var(" << var
518 << ") domain: " << var_domain << " linear1: " << lin;
519
520 switch (lin.type) {
521 case EncodingLinear1Type::kVarEqValue:
522 values.AddReferencedValueToEncode(lin.value);
523 if (push_down_when_unconstrained) {
524 if (lin.value < var_domain.Max()) {
526 var_domain.ValueAtOrAfter(lin.value + 1));
527 }
528 } else {
529 if (lin.value > var_domain.Min()) {
531 var_domain.ValueAtOrBefore(lin.value - 1));
532 }
533 }
534 break;
535 case EncodingLinear1Type::kVarNeValue:
536 values.AddReferencedValueToEncode(lin.value);
537 if (push_down_when_unconstrained) {
538 if (lin.value < var_domain.Max()) {
540 var_domain.ValueAtOrAfter(lin.value + 1));
541 }
542 } else {
543 if (lin.value > var_domain.Min()) {
545 var_domain.ValueAtOrBefore(lin.value - 1));
546 }
547 }
548 break;
549 case EncodingLinear1Type::kVarGeValue: {
550 values.AddReferencedValueToEncode(lin.value);
552 var_domain.ValueAtOrBefore(lin.value - 1));
553 order.InsertGeLiteral(lin.value, lin.enforcement_literal);
554 break;
555 }
556 case EncodingLinear1Type::kVarLeValue: {
557 values.AddReferencedValueToEncode(lin.value);
559 var_domain.ValueAtOrAfter(lin.value + 1));
560 order.InsertLeLiteral(lin.value, lin.enforcement_literal);
561 break;
562 }
563 case EncodingLinear1Type::kVarInDomain: {
564 // TODO(user): fine grained management of the domains.
565 break;
566 }
567 }
568
569 linear_ones_by_type[static_cast<int>(lin.type)].push_back(lin);
570 }
571 const bool has_le_ge_linear1 =
572 !linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarGeValue)]
573 .empty() ||
574 !linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarLeValue)]
575 .empty();
577 push_down_when_unconstrained, has_le_ge_linear1);
578 return true;
579}
580
582 SolutionCrush& solution_crush) {
583 const Domain var_domain = context->DomainOf(var);
584 std::vector<std::vector<EncodingLinear1>> linear_ones_by_type(
585 kNumEncodingLinear1Types);
586 ValueEncoding values(var, context);
587 OrderEncoding order(var, context, solution_crush);
588 bool var_in_objective = false;
589 bool var_has_positive_objective_coefficient = false;
590 std::vector<int> constraint_indices;
592 var, context, values, order, linear_ones_by_type, constraint_indices,
593 var_in_objective, var_has_positive_objective_coefficient)) {
594 return;
595 }
596
597 // Helpers to get the linear1 of each type for var.
598 const auto& lin_eq =
599 linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarEqValue)];
600 const auto& lin_ne =
601 linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarNeValue)];
602 const auto& lin_ge =
603 linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarGeValue)];
604 const auto& lin_le =
605 linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarLeValue)];
606 const auto& lin_domain =
607 linear_ones_by_type[static_cast<int>(EncodingLinear1Type::kVarInDomain)];
608
609 // We force the full encoding if the variable is mostly encoded and some
610 // linear1 involves domains that do not correspond to value or order
611 // encodings.
612 const bool full_encoding_is_not_too_expensive =
613 context->IsMostlyFullyEncoded(var) || var_domain.Size() <= 32;
614 const bool full_encoding_is_needed =
615 (!lin_domain.empty() ||
616 (var_in_objective && context->ObjectiveDomainIsConstraining()));
617 if (!values.is_fully_encoded() && full_encoding_is_not_too_expensive &&
618 full_encoding_is_needed) {
619 VLOG(3) << "Forcing full encoding of var: " << var;
620 values.ForceFullEncoding();
621 }
622
623 if (values.empty()) {
624 // This variable has no value encoding. Either enforced_domains is
625 // empty, and in that case, we will not do anything about it, or the
626 // variable is not used anymore, and it will be removed later.
627 return;
628 }
629
630 VLOG(2) << "ProcessVariableOnlyUsedInEncoding(): var(" << var
631 << "): " << var_domain << ", size: " << var_domain.Size()
632 << ", #encoded_values: " << values.encoded_values().size()
633 << ", #ordered_values: " << order.num_encoded_values()
634 << ", #var_eq_value: " << lin_eq.size()
635 << ", #var_ne_value: " << lin_ne.size()
636 << ", #var_ge_value: " << lin_ge.size()
637 << ", #var_le_value: " << lin_le.size()
638 << ", #var_in_domain: " << lin_domain.size()
639 << ", var_in_objective: " << var_in_objective
640 << ", var_has_positive_objective_coefficient: "
641 << var_has_positive_objective_coefficient;
642 if (full_encoding_is_needed &&
643 (!values.is_fully_encoded() ||
644 var_domain.Size() * lin_domain.size() > 2500)) {
645 VLOG(2) << "Abort - fully_encode_var: " << values.is_fully_encoded()
646 << ", full_encoding_is_not_too_expensive: "
647 << full_encoding_is_not_too_expensive
648 << ", full_encoding_is_needed: " << full_encoding_is_needed;
649 if (var_in_objective) {
650 context->UpdateRuleStats(
651 "TODO variables: only used in objective and in complex encodings");
652 } else {
653 context->UpdateRuleStats(
654 "TODO variables: only used in large complex encodings");
655 }
656 return;
657 }
658
660 // Fix the hinted value if needed.
661 //
662 // The logic follows the classes of equivalence induced by the value of the
663 // literals from the enforced linear1 constraining this variable.
664 // Two values are in the same class if all the literals have the same value.
665 //
666 // We have a heuristic method here:
667 // - If the variable is in the domain, we do nothing.
668 // - If the variable has only var==value and var!=value encodings. All values
669 // not touched by these linear1 are equivalent. We will reassign them to the
670 // unique escape value.
671 // - If the variable also has var>=value and var<=value encodings, we will
672 // push the value of the variable to the closest value in the domain in the
673 // direction of the objective. To this effect, for every contiguous set of
674 // values not in the set of referenced values. the min of the max of that
675 // set has been added to the encoded domain, such that the push up or down
676 // always falls back on an encoded value.
677 //
678 // TODO(user): we could optimize this as, for instance, we only need to
679 // look at values from the order encodings, and not all values when creating
680 // the equivalence class in the last case.
681 const bool push_down_when_unconstrained =
682 !var_in_objective || var_has_positive_objective_coefficient;
684 var, Domain::FromValues(values.encoded_values()),
685 values.unique_escape_value(), push_down_when_unconstrained,
686 values.encoding());
687 order.CreateAllOrderEncodingLiterals(values);
688
689 // Link all Boolean in our linear1 to the encoding literals.
690 for (const EncodingLinear1& info_eq : lin_eq) {
691 context->AddImplication(info_eq.enforcement_literal,
692 values.literal(info_eq.value));
693 }
694
695 for (const EncodingLinear1& info_ne : lin_ne) {
696 context->AddImplication(info_ne.enforcement_literal,
697 NegatedRef(values.literal(info_ne.value)));
698 }
699
700 for (const EncodingLinear1& info_ge : lin_ge) {
701 context->AddImplication(info_ge.enforcement_literal,
702 order.ge_literal(info_ge.value));
703 }
704
705 for (const EncodingLinear1& info_le : lin_le) {
706 context->AddImplication(info_le.enforcement_literal,
707 order.le_literal(info_le.value));
708 }
709
710 for (const EncodingLinear1& info_in : lin_domain) {
711 ConstraintProto* forces =
712 context->AddEnforcedConstraint({info_in.enforcement_literal});
713 for (const int64_t v : info_in.rhs.Values()) {
714 forces->mutable_bool_or()->add_literals(values.literal(v));
715 }
716
717 ConstraintProto* remove =
718 context->AddEnforcedConstraint({info_in.enforcement_literal});
719 const Domain implied_complement =
720 var_domain.IntersectionWith(info_in.rhs.Complement());
721 for (const int64_t v : implied_complement.Values()) {
722 remove->mutable_bool_and()->add_literals(NegatedRef(values.literal(v)));
723 }
724 }
725
726 // Detect implications between complex encodings.
727 const auto add_incompatibility = [context](const EncodingLinear1& info_i,
728 const EncodingLinear1& info_j) {
729 const int e_i = info_i.enforcement_literal;
730 const int e_j = info_j.enforcement_literal;
731 if (e_i == NegatedRef(e_j)) return;
732 BoolArgumentProto* incompatible =
734 incompatible->add_literals(NegatedRef(e_i));
735 incompatible->add_literals(NegatedRef(e_j));
736 context->UpdateRuleStats(
737 "variables: add at_most_one between incompatible complex encodings");
738 };
739
740 for (int i = 0; i < lin_domain.size(); ++i) {
741 const EncodingLinear1& info_i = lin_domain[i];
742 DCHECK_EQ(info_i.type, EncodingLinear1Type::kVarInDomain);
743
744 // Incompatibilities between x in domain and x >= ge.
745 for (const EncodingLinear1& info_j : lin_ge) {
746 DCHECK_EQ(info_j.type, EncodingLinear1Type::kVarGeValue);
747 if (info_i.rhs.Max() < info_j.value) {
748 add_incompatibility(info_i, info_j);
749 }
750 }
751
752 // Incompatibilities between x in domain and x <= value.
753 for (const EncodingLinear1& info_j : lin_le) {
754 DCHECK_EQ(info_j.type, EncodingLinear1Type::kVarLeValue);
755 if (info_i.rhs.Min() > info_j.value) {
756 add_incompatibility(info_i, info_j);
757 }
758 }
759
760 // Incompatibilities between x in domain_i and x in domain_j.
761 for (int j = i + 1; j < lin_domain.size(); ++j) {
762 const EncodingLinear1& info_j = lin_domain[j];
763 DCHECK_EQ(info_j.type, EncodingLinear1Type::kVarInDomain);
764 if (!info_i.rhs.OverlapsWith(info_j.rhs)) {
765 add_incompatibility(info_i, info_j);
766 }
767 }
768 }
770
771 // Update the objective if needed. Note that this operation can fail if
772 // the new expression result in potential overflow.
773 if (var_in_objective) {
774 // We substract the min or the max of the variable from all
775 // coefficients. This should reduce the objective size and helps with
776 // the bounds.
777 const int64_t base_value = var_has_positive_objective_coefficient
778 ? var_domain.Min()
779 : var_domain.Max();
780 // Tricky: We cannot just choose an arbitrary value if the objective has
781 // a restrictive domain!
782 DCHECK(values.is_fully_encoded() ||
784
785 // Checks for overflow before trying to substitute the variable in the
786 // objective.
787 int64_t accumulated = std::abs(base_value);
788 for (const int64_t value : values.encoded_values()) {
789 accumulated = CapAdd(accumulated, std::abs(CapSub(value, base_value)));
790 if (accumulated == std::numeric_limits<int64_t>::max()) {
791 VLOG(2) << "Abort - overflow when converting linear1 to clauses";
792 context->UpdateRuleStats(
793 "TODO variables: overflow when converting linear1 to clauses");
794 return;
795 }
796 }
797
798 // TODO(user): we could also use a log encoding here if the domain is
799 // large and the objective is not constraining.
800 ConstraintProto encoding_ct;
801 LinearConstraintProto* linear = encoding_ct.mutable_linear();
802 const int64_t coeff_in_equality = -1;
803 linear->add_vars(var);
804 linear->add_coeffs(coeff_in_equality);
805 int64_t rhs_value = -base_value;
806 for (const auto& [value, literal] : values.encoding()) {
807 const int64_t coeff = value - base_value;
808 if (coeff == 0) continue;
809 if (RefIsPositive(literal)) {
810 linear->add_vars(literal);
811 linear->add_coeffs(coeff);
812 } else {
813 // (1 - var) * coeff;
814 rhs_value -= coeff;
815 linear->add_vars(PositiveRef(literal));
816 linear->add_coeffs(-coeff);
817 }
818 }
819 linear->add_domain(rhs_value);
820 linear->add_domain(rhs_value);
821 if (!context->SubstituteVariableInObjective(var, coeff_in_equality,
822 encoding_ct)) {
823 context->UpdateRuleStats(
824 "TODO variables: cannot substitute encoded variable in objective");
825 return;
826 }
827 context->UpdateRuleStats(
828 "variables: only used in objective and in encodings");
829 } else {
830 if ((!lin_eq.empty() || !lin_ne.empty()) && lin_domain.empty()) {
831 context->UpdateRuleStats(
832 "variables: only used in value and order encodings");
833 } else if (!lin_domain.empty()) {
834 context->UpdateRuleStats("variables: only used in complex encodings");
835 } else {
836 context->UpdateRuleStats("variables: only used in value encodings");
837 }
838 }
839 if (!values.is_fully_encoded()) {
840 VLOG(2) << "Reduce domain size: " << var_domain.Size() << " to "
841 << values.encoded_values().size() << ": " << var_domain << " -> "
843 context->UpdateRuleStats("variables: reduce domain to encoded values");
844 }
845
846 // Clear all involved constraint. We do it in two passes to avoid
847 // invalidating the iterator. We also use the constraint variable graph as
848 // extra encodings (value, order) may have added new constraints.
849 {
850 std::vector<int> to_clear;
851 for (const int c : context->VarToConstraints(var)) {
852 if (c >= 0) to_clear.push_back(c);
853 }
854 absl::c_sort(to_clear);
855 for (const int c : to_clear) {
858 }
859 }
860
861 // This must be done after we removed all the constraint containing var.
864 for (const auto& [value, literal] : values.encoding()) {
865 arg->add_literals(literal);
866 }
868 if (context->ModelIsUnsat()) return;
869
870 // To simplify the postsolve, we output a single constraint to infer X from
871 // the bi: X = sum bi * (Vi - min_value) + min_value
872 const int64_t var_min = var_domain.Min();
873 ConstraintProto* mapping_ct =
874 context->NewMappingConstraint(__FILE__, __LINE__);
875 mapping_ct->mutable_linear()->add_vars(var);
876 mapping_ct->mutable_linear()->add_coeffs(1);
877 int64_t offset = var_min;
878 for (const auto& [value, literal] : values.encoding()) {
879 const int64_t coeff = value - var_min;
880 if (coeff == 0) continue;
881 if (RefIsPositive(literal)) {
882 mapping_ct->mutable_linear()->add_vars(literal);
883 mapping_ct->mutable_linear()->add_coeffs(-coeff);
884 } else {
885 offset += coeff;
886 mapping_ct->mutable_linear()->add_vars(PositiveRef(literal));
887 mapping_ct->mutable_linear()->add_coeffs(coeff);
888 }
889 }
890 mapping_ct->mutable_linear()->add_domain(offset);
891 mapping_ct->mutable_linear()->add_domain(offset);
892
893 context->MarkVariableAsRemoved(var);
894}
895
896} // namespace sat
897} // namespace operations_research
static Domain FromValues(std::vector< int64_t > values)
int64_t ValueAtOrAfter(int64_t input) const
Domain IntersectionWith(const Domain &domain) const
DomainIteratorBeginEnd Values() const &
Domain InverseMultiplicationBy(int64_t coeff) const
int64_t ValueAtOrBefore(int64_t input) const
::operations_research::sat::BoolArgumentProto *PROTOBUF_NONNULL mutable_bool_and()
::int32_t enforcement_literal(int index) const
const ::operations_research::sat::LinearConstraintProto & linear() const
::operations_research::sat::BoolArgumentProto *PROTOBUF_NONNULL mutable_exactly_one()
::operations_research::sat::BoolArgumentProto *PROTOBUF_NONNULL mutable_bool_or()
ABSL_ATTRIBUTE_REINITIALIZES void Clear() PROTOBUF_FINAL
::operations_research::sat::LinearConstraintProto *PROTOBUF_NONNULL mutable_linear()
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL mutable_constraints(int index)
const ::operations_research::sat::ConstraintProto & constraints(int index) const
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL add_constraints()
OrderEncoding(int var, PresolveContext *context, SolutionCrush &solution_crush)
void CreateAllOrderEncodingLiterals(const ValueEncoding &values)
void InsertLeLiteral(int64_t value, int literal)
void InsertGeLiteral(int64_t value, int literal)
ABSL_MUST_USE_RESULT bool SubstituteVariableInObjective(int var_in_equality, int64_t coeff_in_equality, const ConstraintProto &equality)
int NewBoolVar(absl::string_view source)
ConstraintProto * NewMappingConstraint(absl::string_view file, int line)
ConstraintProto * AddEnforcedConstraint(absl::Span< const int > enforcement_literals)
const absl::flat_hash_set< int > & VarToConstraints(int var) const
const absl::flat_hash_map< int, int64_t > & ObjectiveMap() const
void UpdateRuleStats(std::string_view name, int num_times=1)
void MaybeSetLiteralToOrderEncoding(int literal, int var, int64_t value, bool is_le)
void SetOrUpdateVarToDomainWithOptionalEscapeValue(int var, const Domain &reduced_var_domain, std::optional< int64_t > unique_escape_value, bool push_down_when_not_in_domain, const absl::btree_map< int64_t, int > &encoding)
const absl::btree_map< int64_t, int > & encoding() const
const std::vector< int64_t > & encoded_values() const
std::optional< int64_t > unique_escape_value() const
ValueEncoding(int var, PresolveContext *context)
void CanonicalizeEncodedValuesAndAddEscapeValue(bool push_down_when_unconstrained, bool has_le_ge_linear1)
void STLSortAndRemoveDuplicates(T *v, const LessFunc &less_func)
Definition stl_util.h:55
bool ProcessEncodingConstraints(int var, PresolveContext *context, ValueEncoding &values, OrderEncoding &order, std::vector< std::vector< EncodingLinear1 > > &linear_ones_by_type, std::vector< int > &constraint_indices, bool &var_in_objective, bool &var_has_positive_objective_coefficient)
void TryToReplaceVariableByItsEncoding(int var, PresolveContext *context, SolutionCrush &solution_crush)
constexpr int kObjectiveConstraint
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
void AbslStringify(Sink &sink, EdgePosition e)
OR-Tools root namespace.
int64_t CapAdd(int64_t x, int64_t y)
int64_t CapSub(int64_t x, int64_t y)
const bool DEBUG_MODE
Definition radix_sort.h:266
absl::string_view ToString(MPSolver::OptimizationProblemType optimization_problem_type)
int64_t Max() const
Definition model.cc:346
int64_t Min() const
Definition model.cc:340
bool Contains(int64_t value) const
Definition model.cc:363