Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
vivification.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 <algorithm>
17#include <utility>
18#include <vector>
19
20#include "absl/algorithm/container.h"
21#include "absl/cleanup/cleanup.h"
22#include "absl/container/btree_set.h"
23#include "absl/log/check.h"
24#include "absl/log/log.h"
25#include "absl/types/span.h"
26#include "ortools/sat/clause.h"
28#include "ortools/sat/util.h"
30
32
33bool Vivifier::MinimizeByPropagation(bool log_info, double dtime_budget,
34 bool minimize_new_clauses_only) {
35 PresolveTimer timer("Vivification", logger_, time_limit_);
36 timer.OverrideLogging(log_info || VLOG_IS_ON(2));
37
38 sat_solver_->AdvanceDeterministicTime(time_limit_);
39 const double threshold =
40 time_limit_->GetElapsedDeterministicTime() + dtime_budget;
41
42 // TODO(user): Fix that. For now the solver cannot be used properly to
43 // minimize clauses if assumption_level_ is not zero.
44 if (sat_solver_->AssumptionLevel() > 0) return true;
45
46 // Tricky: we don't want TryToMinimizeClause() to delete to_minimize
47 // while we are processing it.
48 sat_solver_->BlockClauseDeletion(true);
49 absl::Cleanup unblock_clause_deletion = [&] {
50 sat_solver_->BlockClauseDeletion(false);
51 };
52
53 const auto old_counter = counters_;
54 const int num_resets = clause_manager_->NumToMinimizeIndexResets();
55 while (!time_limit_->LimitReached()) {
56 // Abort if we used our budget.
57 sat_solver_->AdvanceDeterministicTime(time_limit_);
58 if (time_limit_->GetElapsedDeterministicTime() >= threshold) break;
59
60 // Also abort if we did more than one loop over all the clause.
61 if (clause_manager_->NumToMinimizeIndexResets() > num_resets + 1) break;
62
63 // First minimize clauses that where never minimized before.
64 {
65 SatClause* to_minimize = clause_manager_->NextNewClauseToMinimize();
66 if (to_minimize != nullptr) {
67 if (!TryToMinimizeClause(to_minimize)) return false;
68 continue;
69 }
70 if (minimize_new_clauses_only) break; // We are done.
71 }
72
73 SatClause* clause = clause_manager_->NextClauseToMinimize();
74 if (clause != nullptr) {
75 if (!TryToMinimizeClause(clause)) return false;
76 }
77 }
78
79 // Note(user): In some corner cases, the function above might find a
80 // feasible assignment. I think it is okay to ignore this special case
81 // that should only happen on trivial problems and just reset the solver.
82 const bool result = sat_solver_->ResetToLevelZero();
83
84 last_num_vivified_ =
85 counters_.num_clauses_vivified - old_counter.num_clauses_vivified;
86 last_num_literals_removed_ =
87 counters_.num_removed_literals - old_counter.num_removed_literals;
88 timer.AddCounter("num_vivifed", last_num_vivified_);
89 timer.AddCounter("literals_removed", last_num_literals_removed_);
90 timer.AddCounter("loops", clause_manager_->NumToMinimizeIndexResets());
91
92 clause_manager_->DeleteRemovedClauses();
93 return result;
94}
95
96void Vivifier::KeepAllClausesUsedToInfer(BooleanVariable variable) {
97 CHECK(assignment_.VariableIsAssigned(variable));
98 if (trail_->Info(variable).level == 0) return;
99 int trail_index = trail_->Info(variable).trail_index;
100 std::vector<bool> is_marked(trail_index + 1, false); // move to local member.
101 is_marked[trail_index] = true;
102 int num = 1;
103 for (; num > 0 && trail_index >= 0; --trail_index) {
104 if (!is_marked[trail_index]) continue;
105 is_marked[trail_index] = false;
106 --num;
107
108 const BooleanVariable var = (*trail_)[trail_index].Variable();
109 SatClause* clause = clause_manager_->ReasonClauseOrNull(var);
110 if (clause != nullptr) {
111 clause_manager_->KeepClauseForever(clause);
112 }
114 continue;
115 }
116 for (const Literal l : trail_->Reason(var)) {
117 const AssignmentInfo& info = trail_->Info(l.Variable());
118 if (info.level == 0) continue;
119 if (!is_marked[info.trail_index]) {
120 is_marked[info.trail_index] = true;
121 ++num;
122 }
123 }
124 }
125}
126
127bool Vivifier::SubsumptionIsInteresting(BooleanVariable variable,
128 int max_size) {
129 // TODO(user): other id should probably be safe as long as we do not delete
130 // the propagators. Note that symmetry is tricky since we would need to keep
131 // the symmetric clause around in KeepAllClauseUsedToInfer().
132 const int binary_id = binary_clauses_->PropagatorId();
133 const int clause_id = clause_manager_->PropagatorId();
134
135 CHECK(assignment_.VariableIsAssigned(variable));
136 if (trail_->Info(variable).level == 0) return true;
137 int trail_index = trail_->Info(variable).trail_index;
138 std::vector<bool> is_marked(trail_index + 1, false); // move to local member.
139 is_marked[trail_index] = true;
140 int num = 1;
141 int num_clause_to_mark_as_non_deletable = 0;
142 for (; num > 0 && trail_index >= 0; --trail_index) {
143 if (!is_marked[trail_index]) continue;
144 is_marked[trail_index] = false;
145 --num;
146
147 const BooleanVariable var = (*trail_)[trail_index].Variable();
148 const int type = trail_->AssignmentType(var);
149 if (type == AssignmentType::kSearchDecision) continue;
150 if (type != binary_id && type != clause_id) return false;
151 SatClause* clause = clause_manager_->ReasonClauseOrNull(var);
152 if (clause != nullptr && clause_manager_->IsRemovable(clause)) {
153 if (clause->size() > max_size) {
154 return false;
155 }
156 if (++num_clause_to_mark_as_non_deletable > 1) return false;
157 }
158 for (const Literal l : trail_->Reason(var)) {
159 const AssignmentInfo& info = trail_->Info(l.Variable());
160 if (info.level == 0) continue;
161 if (!is_marked[info.trail_index]) {
162 is_marked[info.trail_index] = true;
163 ++num;
164 }
165 }
166 }
167 return num_clause_to_mark_as_non_deletable <= 1;
168}
169
170// This implements "vivification" as described in
171// https://doi.org/10.1016/j.artint.2019.103197, with one significant tweak:
172// we sort each clause by current trail index before trying to minimize it so
173// that we can reuse the trail from previous calls in case there are overlaps.
174bool Vivifier::TryToMinimizeClause(SatClause* clause) {
175 CHECK(clause != nullptr);
176 if (clause->empty()) return true;
177 ++counters_.num_clauses_vivified;
178
179 // TODO(user): Make sure clause do not contain any redundant literal before
180 // we try to minimize it.
181 std::vector<Literal> candidate;
182 candidate.reserve(clause->size());
183
184 // Some literals of the clause which are fixed to false or true when
185 // propagating some other literals of the clause. Only used if
186 // lrat_proof_handler_ is not null.
187 std::vector<Literal> fixed_false_literals;
188 LiteralIndex fixed_true_literal = kNoLiteralIndex;
189
190 // Note that CP-SAT presolve detects clauses that share n-1 literals and
191 // transforms them into (n-1 enforcement) => (1 literal per clause). We
192 // currently do not support that internally, but these clauses will still
193 // likely be loaded one after the other, so there is a high chance that if we
194 // call TryToMinimizeClause() on consecutive clauses, there will be a long
195 // prefix in common!
196 //
197 // TODO(user): Exploit this more by choosing a good minimization order?
198 int longest_valid_prefix = 0;
199 if (sat_solver_->CurrentDecisionLevel() > 0) {
200 candidate.resize(clause->size());
201
202 // Insert any compatible decisions into their correct place in candidate
203 const auto& decisions = trail_->Decisions();
204 for (Literal lit : *clause) {
205 if (!assignment_.LiteralIsFalse(lit)) continue;
206 const AssignmentInfo& info = trail_->Info(lit.Variable());
207 if (info.level <= 0 || info.level > clause->size()) continue;
208 if (decisions[info.level - 1].literal == lit.Negated()) {
209 candidate[info.level - 1] = lit;
210 }
211 }
212 // Then compute the matching prefix and discard the rest
213 for (int i = 0; i < candidate.size(); ++i) {
214 if (candidate[i] != Literal()) {
215 ++longest_valid_prefix;
216 } else {
217 break;
218 }
219 }
220 counters_.num_reused += longest_valid_prefix;
221 candidate.resize(longest_valid_prefix);
222 }
223 // Then do a second pass to add the remaining literals in order.
224 for (Literal lit : *clause) {
225 const AssignmentInfo& info = trail_->Info(lit.Variable());
226 // Skip if this literal is already in the prefix.
227 if (info.level >= 1 && info.level <= longest_valid_prefix &&
228 candidate[info.level - 1] == lit) {
229 continue;
230 }
231 candidate.push_back(lit);
232 }
233 CHECK_EQ(candidate.size(), clause->size());
234
235 if (!sat_solver_->BacktrackAndPropagateReimplications(longest_valid_prefix)) {
236 return false;
237 }
238
239 absl::btree_set<LiteralIndex> moved_last;
240 while (!sat_solver_->ModelIsUnsat()) {
241 // We want each literal in candidate to appear last once in our propagation
242 // order. We want to do that while maximizing the reutilization of the
243 // current assignment prefix, that is minimizing the number of
244 // decision/progagation we need to perform.
245 const int target_level = MoveOneUnprocessedLiteralLast(
246 moved_last, sat_solver_->CurrentDecisionLevel(), &candidate);
247 if (target_level == -1) break;
248 if (!sat_solver_->BacktrackAndPropagateReimplications(target_level)) {
249 return false;
250 }
251 fixed_false_literals.clear();
252 fixed_true_literal = kNoLiteralIndex;
253
254 while (sat_solver_->CurrentDecisionLevel() < candidate.size()) {
255 if (time_limit_->LimitReached()) return true;
256 const int level = sat_solver_->CurrentDecisionLevel();
257 const Literal literal = candidate[level];
258 // Remove false literals
259 if (assignment_.LiteralIsFalse(literal)) {
260 if (lrat_proof_handler_ != nullptr) {
261 fixed_false_literals.push_back(literal);
262 }
263 candidate[level] = candidate.back();
264 candidate.pop_back();
265 continue;
266 } else if (assignment_.LiteralIsTrue(literal)) {
267 const int variable_level = trail_->Info(literal.Variable()).level;
268 if (variable_level == 0) {
269 DCHECK(lrat_proof_handler_ == nullptr ||
270 trail_->GetUnitClauseId(literal.Variable()) != kNoClauseId);
271 counters_.num_true++;
272 counters_.num_removed_literals += clause->size();
273 clause_manager_->LazyDelete(clause,
275 return true;
276 }
277
278 if (parameters_.inprocessing_minimization_use_conflict_analysis()) {
279 // Replace the clause with the reason for the literal being true, plus
280 // the literal itself.
281 candidate.clear();
282 for (const Literal l : sat_solver_->GetDecisionsFixing({literal})) {
283 candidate.push_back(l.Negated());
284 }
285 } else {
286 candidate.resize(variable_level);
287 }
288 fixed_true_literal = literal.Index();
289 candidate.push_back(literal);
290
291 // If a (true) literal wasn't propagated by this clause, then we know
292 // that this clause is subsumed by other clauses in the database, so we
293 // can remove it so long as the subsumption is due to non-removable
294 // clauses. If we can subsume this clause by making only 1 additional
295 // clause permanent and that clause is no longer than this one, we will
296 // do so.
297 if (parameters_.subsume_during_vivification() &&
298 clause_manager_->ReasonClauseOrNull(literal.Variable()) != clause &&
299 SubsumptionIsInteresting(literal.Variable(), candidate.size())) {
300 counters_.num_subsumed++;
301 counters_.num_removed_literals += clause->size();
302 KeepAllClausesUsedToInfer(literal.Variable());
303 clause_manager_->LazyDelete(
305 return true;
306 }
307
308 break;
309 } else {
310 ++counters_.num_decisions;
311 sat_solver_->EnqueueDecisionAndBackjumpOnConflict(literal.Negated());
312 if (sat_solver_->ModelIsUnsat()) return false;
313 if (clause->IsRemoved()) return true;
314
315 if (sat_solver_->CurrentDecisionLevel() < level) {
316 ++counters_.num_conflicts;
317
318 // There was a conflict, consider the conflicting literal next so we
319 // should be able to exploit the conflict in the next iteration.
320 // TODO(user): I *think* this is sufficient to ensure pushing
321 // the same literal to the new trail fails, immediately on the next
322 // iteration, if not we may be able to analyse the last failure and
323 // skip some propagation steps?
324 std::swap(candidate[level],
325 candidate[sat_solver_->CurrentDecisionLevel()]);
326 }
327 }
328 }
329 if (candidate.empty()) {
330 sat_solver_->NotifyThatModelIsUnsat();
331 return false;
332 }
333
334 // TODO(user): To use this, we need to proove and rewrite the clause
335 // on each of its modification.
336 if (!parameters_.inprocessing_minimization_use_all_orderings()) break;
337 moved_last.insert(candidate.back().Index());
338 }
339
340 // Returns if we don't have any minimization.
341 if (candidate.size() == clause->size()) return true;
342
343 std::vector<ClauseId> clause_ids;
344 if (lrat_proof_handler_ != nullptr) {
345 DCHECK(fixed_true_literal != kNoLiteralIndex ||
346 !fixed_false_literals.empty());
347 clause_ids.clear();
348 if (fixed_true_literal != kNoLiteralIndex) {
349 // If some literals of the minimized clause fix another to true, we just
350 // need the propagating clauses to prove this (assuming that all the
351 // minimized clause literals are false will lead to a conflict on this
352 // 'fixed to true' literal).
353 clause_manager_->AppendClauseIdsFixing({Literal(fixed_true_literal)},
354 &clause_ids);
355 } else {
356 // If some literals of the minimized clause fix those that have been
357 // removed to false, the propagating clauses and the original one prove
358 // this (assuming that all the minimized clause literals are false will
359 // lead to all the literals of the original clause fixed to false, which
360 // is a conflict with the original clause).
361 clause_manager_->AppendClauseIdsFixing(fixed_false_literals, &clause_ids);
362 clause_ids.push_back(clause_manager_->GetClauseId(clause));
363 }
364 }
365
366 // Reverse the candidate so that the first two literals are appropriate
367 // watchers.
368 absl::c_reverse(candidate);
369 // All but the first literal of the new clause should be false.
370 DCHECK(absl::c_all_of(absl::MakeSpan(candidate).subspan(1), [&](Literal l) {
371 return assignment_.LiteralIsFalse(l);
372 }));
373 if (candidate.size() == 1) {
374 sat_solver_->BacktrackAndPropagateReimplications(0);
375 } else if (assignment_.LiteralIsFalse(candidate[1]) &&
376 (!assignment_.LiteralIsTrue(candidate[0]) ||
377 trail_->AssignmentLevel(candidate[1]) <
378 trail_->AssignmentLevel(candidate[0]))) {
379 // Backtrack if the new clause would propagate earlier than the current
380 // reason. This should be a very rare edge case, but it can happen if both
381 // conflicts and clause cleanup occur during minimization: some literal in
382 // the clause that was propagated false by some decisions might no longer be
383 // propagated by the same decisions after backjumping because the clause
384 // that propagated it was removed.
385 // Note we backtrack to 1 level before this would propagate because we don't
386 // actually support propagating the new clause during rewrite, and the
387 // propagation would probably be useless.
388 sat_solver_->BacktrackAndPropagateReimplications(
389 std::max(0, trail_->AssignmentLevel(candidate[1])));
390 }
391 if (sat_solver_->CurrentDecisionLevel() == 0) {
392 // Ensure nothing is fixed at level 0 in case more propagation happened
393 // after backtracking.
394 OpenSourceEraseIf(candidate,
395 [&](Literal l) { return assignment_.LiteralIsFalse(l); });
396 if (absl::c_any_of(clause->AsSpan(), [&](Literal l) {
397 return assignment_.LiteralIsTrue(l);
398 })) {
399 counters_.num_removed_literals += clause->size();
400 clause_manager_->LazyDelete(clause, DeletionSourceForStat::FIXED_AT_TRUE);
401 return true;
402 }
403 }
404
405 counters_.num_removed_literals += clause->size() - candidate.size();
406 if (!clause_manager_->InprocessingRewriteClause(clause, candidate,
407 clause_ids)) {
408 sat_solver_->NotifyThatModelIsUnsat();
409 return false;
410 }
411
412 // Adding a unit clause can cause additional propagation. There is also an
413 // edge case where we added the first binary clause of the model by
414 // strenghtening a normal clause.
415 return sat_solver_->FinishPropagation();
416}
417
418} // namespace operations_research::sat
void OverrideLogging(bool value)
Definition logging.h:160
void AddCounter(std::string name, int64_t count)
Definition logging.h:147
SatClause * ReasonClauseOrNull(BooleanVariable var) const
Definition clause.cc:299
void KeepClauseForever(SatClause *const clause)
Definition clause.h:266
int AssignmentType(BooleanVariable var) const
Definition sat_base.h:942
absl::Span< const Literal > Reason(BooleanVariable var, int64_t conflict_id=-1) const
Definition sat_base.h:951
const AssignmentInfo & Info(BooleanVariable var) const
Definition sat_base.h:655
bool VariableIsAssigned(BooleanVariable var) const
Definition sat_base.h:214
ABSL_MUST_USE_RESULT bool MinimizeByPropagation(bool log_info, double dtime_budget, bool minimize_new_clauses_only=false)
void OpenSourceEraseIf(Container &c, Pred pred)
Definition util.h:55
const LiteralIndex kNoLiteralIndex(-1)
constexpr ClauseId kNoClauseId(0)
int MoveOneUnprocessedLiteralLast(const absl::btree_set< LiteralIndex > &processed, int relevant_prefix_size, std::vector< Literal > *literals)
Definition util.cc:377