Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sat_sweeping.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 <array>
18#include <deque>
19#include <functional>
20#include <utility>
21#include <vector>
22
23#include "absl/algorithm/container.h"
24#include "absl/base/log_severity.h"
25#include "absl/cleanup/cleanup.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/log/check.h"
28#include "absl/log/log.h"
29#include "absl/random/distributions.h"
30#include "absl/strings/string_view.h"
31#include "absl/types/span.h"
33#include "ortools/base/timer.h"
34#include "ortools/sat/clause.h"
36#include "ortools/sat/model.h"
40#include "ortools/sat/util.h"
42
43namespace operations_research {
44namespace sat {
45
46void EquivalenceSatSweeping::LoadClausesInModel(
47 absl::Span<const SatClause* const> clauses, Model* m) {
48 const int num_booleans = big_model_to_small_model_.size();
49 auto* sat_solver = m->GetOrCreate<SatSolver>();
50 CHECK_EQ(sat_solver->NumVariables(), 0);
51
52 sat_solver->SetNumVariables(num_booleans);
53
54 std::vector<Literal> literals;
55 for (const SatClause* clause : clauses) {
56 literals.clear();
57 for (const Literal l : clause->AsSpan()) {
58 literals.push_back(
59 Literal(big_model_to_small_model_[l.Variable()], l.IsPositive()));
60 }
61 sat_solver->AddProblemClause(literals);
62 }
63}
64
65// We define the neighborhood of clauses and variables with respect to `v` by
66// defining a distance between variables and getting the closest variables to it
67// and the clauses linking the neighborhood variables. The distance is defined
68// as a non-negative integer satisfying:
69// - d(v1, v2) == 0 iff v1 == v2.
70// - d(v1, v2) == 1 iff v1 and v2 appears in the same clause.
71// - d(v1, v2) satisfies triangle inequality.
72//
73// Note that the distance above is equivalent to the distance on the graph of
74// clauses.
75std::vector<absl::Span<const Literal>> EquivalenceSatSweeping::GetNeighborhood(
76 BooleanVariable var) {
77 std::vector<absl::Span<const Literal>> neighborhood;
78 absl::flat_hash_set<BooleanVariable> seen_bools = {var};
79 const int binary_clause_slack = max_num_clauses_ / 10;
80 std::deque<BooleanVariable> bools_to_add;
81 bools_to_add.push_back(var);
82 while (!bools_to_add.empty()) {
83 // TODO(user): when all the variables and clauses of a given distance
84 // don't fit in the limit we are picking which ones we put in the
85 // neighborhood quite arbitrarily. We should try doing it using a priority
86 // queue of how many clauses they using the variable or do it randomly.
87 const BooleanVariable cur_var = bools_to_add.front();
88 bools_to_add.pop_front();
89 for (const ClauseIndex clause_index : var_to_clauses_[cur_var]) {
90 const absl::Span<const Literal> cur_clause = clauses_[clause_index];
91 const int num_unseen_bools = absl::c_count_if(cur_clause, [&](Literal l) {
92 return !seen_bools.contains(l.Variable());
93 });
94 if (seen_bools.size() + num_unseen_bools >= max_num_boolean_variables_) {
95 continue;
96 }
97 if (cur_clause.size() == 2) {
98 const Literal other_lit =
99 cur_clause[0].Variable() == cur_var ? cur_clause[1] : cur_clause[0];
100 if (implication_graph_->RepresentativeOf(other_lit).Variable() ==
101 cur_var) {
102 // Do not waste our variable budget with non-representative literals
103 // and the clauses mapping them to their representative. We might end
104 // up with a neighborhood that is too small if the inprocessing did
105 // not yet replaced the literals with their representative, but it's
106 // better than wasting effort.
107 continue;
108 }
109 }
110 if (neighborhood.size() >= max_num_clauses_ - binary_clause_slack &&
111 cur_clause.size() > 2) {
112 // Reserve a bit of out clauses budget for binary clauses. We do not
113 // want to waste resources rediscovering them.
114 continue;
115 }
116 for (const Literal l : cur_clause) {
117 if (seen_bools.contains(l.Variable())) continue;
118 bools_to_add.push_back(l.Variable());
119 seen_bools.insert(l.Variable());
120 }
121 neighborhood.push_back(cur_clause);
122 if (neighborhood.size() >= max_num_clauses_) return neighborhood;
123 }
124 }
125 return neighborhood;
126}
127
128namespace {
129void RefinePartitions(std::vector<std::vector<Literal>>& partitions,
131 // TODO(user): check whether we can use
132 // google3/ortools/algorithms/dynamic_partition.h
133 const int original_num_partitions = partitions.size();
134 for (int i = 0; i < original_num_partitions; i++) {
135 std::vector<Literal>& partition_for_negated = partitions.emplace_back();
136 std::vector<Literal>& partition_for_true = partitions[i];
137 // Split the partition in two, according to the value of the literals in the
138 // solution.
139 int new_partition_for_true_size = 0;
140 for (int j = 0; j < partition_for_true.size(); j++) {
141 const Literal lit = partition_for_true[j];
142 if (!solution.LiteralIsTrue(lit)) {
143 partition_for_negated.push_back(lit);
144 continue;
145 }
146 partition_for_true[new_partition_for_true_size++] = lit;
147 }
148 partition_for_true.resize(new_partition_for_true_size);
149 // Partitions of size 1 are useless.
150 if (partition_for_negated.size() <= 1) {
151 partitions.pop_back();
152 }
153 }
154 int new_num_partitions = 0;
155 for (int i = 0; i < partitions.size(); i++) {
156 if (partitions[i].size() > 1) {
157 if (new_num_partitions != i) {
158 partitions[new_num_partitions] = std::move(partitions[i]);
159 }
160 new_num_partitions++;
161 }
162 }
163 partitions.resize(new_num_partitions);
164}
165} // namespace
166
168 std::function<void(Model*)> run_inprocessing) {
169 // For now we compute a single neighborhood and do a full SAT sweeping on it.
170 // TODO(user): consider doing several neighborhoods to amortize the cost of
171 // building the variable->clause graph.
172 CHECK_EQ(sat_solver_->CurrentDecisionLevel(), 0);
173 if (sat_solver_->AssumptionLevel() != 0) {
174 VLOG(2)
175 << "Assumption level is not 0 (should not happen), skipping sweeping.";
176 return true;
177 }
178 clauses_.clear();
179
180 struct ExtractedClausesHelper {
181 explicit ExtractedClausesHelper(
182 util_intops::StrongVector<ClauseIndex, absl::Span<const Literal>>&
183 clauses_vec,
184 int clause_size_limit_var)
185 : clause_size_limit(clause_size_limit_var), clauses(clauses_vec) {}
186 void AddBinaryClause(Literal a, Literal b) {
187 binary_clauses.push_back({a, b});
188 clauses.push_back(absl::MakeConstSpan(binary_clauses.back()));
189 }
190 void AddClause(absl::Span<const Literal> clause) {
191 if (clause.size() > clause_size_limit) return;
192 clauses.push_back(clause);
193 }
194 void SetNumVariables(int /* unused */) {}
195
196 int clause_size_limit;
197 // Use a deque for pointer stability.
198 std::deque<std::array<Literal, 2>> binary_clauses;
200 };
201
202 ExtractedClausesHelper helper(clauses_, max_num_boolean_variables_);
203 if (!sat_solver_->ExtractClauses(&helper)) return false;
204
205 if (clauses_.empty()) {
206 VLOG(2) << "No clauses extracted, skipping sweeping.";
207 return true;
208 }
209
210 const int num_vars = sat_solver_->NumVariables();
211
212 struct GetVarMapper {
213 BooleanVariable operator()(Literal l) const { return l.Variable(); }
214 };
215 var_to_clauses_.ResetFromTransposeMap<GetVarMapper>(clauses_, num_vars);
216
217 global_time_limit_->AdvanceDeterministicTime(clause_manager_->num_clauses() *
218 1.0e-7);
219 TimeLimit sweep_time_limit;
220 sweep_time_limit.ChangeDeterministicLimit(1.0);
221 sweep_time_limit.MergeWithGlobalTimeLimit(global_time_limit_);
222 std::vector<std::pair<Literal, Literal>> binary_clauses;
223 std::vector<Literal> unary_clauses;
224 for (int i = 0; i < 50; ++i) {
225 BooleanVariable boolean_for_neighborhood;
226 {
227 int tries = 0;
228 constexpr int kMaxTries = 10;
229 for (tries = 0; tries < kMaxTries; ++tries) {
230 boolean_for_neighborhood = absl::Uniform<int>(*random_, 0, num_vars);
231 if (var_to_clauses_[boolean_for_neighborhood].size() < 2) continue;
232 const Literal positive_lit(boolean_for_neighborhood, true);
233 if (implication_graph_->RepresentativeOf(positive_lit) !=
234 positive_lit) {
235 continue;
236 }
237 break;
238 }
239 if (tries == kMaxTries) continue;
240 }
241
242 const std::vector<absl::Span<const Literal>> neighborhood =
243 GetNeighborhood(boolean_for_neighborhood);
244
245 if (neighborhood.empty()) {
246 VLOG(2) << "Neighborhood is empty for " << boolean_for_neighborhood;
247 continue;
248 }
249
250 CompactVectorVector<int, Literal> neighborhood_clauses;
251 big_model_to_small_model_.clear();
252 small_model_to_big_model_.clear();
253 for (const absl::Span<const Literal> clause : neighborhood) {
254 neighborhood_clauses.Add({});
255 for (const Literal l : clause) {
256 const BooleanVariable new_var(big_model_to_small_model_.size());
257 auto [it, inserted] =
258 big_model_to_small_model_.insert({l.Variable(), new_var});
259 if (inserted) {
260 small_model_to_big_model_.push_back(l.Variable());
261 }
262 neighborhood_clauses.AppendToLastVector(
263 Literal(it->second, l.IsPositive()));
264 }
265 }
266
268 neighborhood_clauses, &sweep_time_limit, run_inprocessing);
269
270 if (result.status == SatSolver::INFEASIBLE) {
271 sat_solver_->NotifyThatModelIsUnsat();
272 return false;
273 }
274 for (const auto& [l1, l2] : result.binary_clauses) {
275 const Literal mapped_l1 =
276 Literal(small_model_to_big_model_[l1.Variable()], l1.IsPositive());
277 const Literal mapped_l2 =
278 Literal(small_model_to_big_model_[l2.Variable()], l2.IsPositive());
279 if (implication_graph_->IsRemoved(mapped_l1) ||
280 implication_graph_->IsRemoved(mapped_l2)) {
281 continue;
282 }
283 binary_clauses.push_back({mapped_l1, mapped_l2});
284 }
285 for (const Literal l : result.unary_clauses) {
286 const Literal mapped_l =
287 Literal(small_model_to_big_model_[l.Variable()], l.IsPositive());
288 if (implication_graph_->IsRemoved(mapped_l)) continue;
289 unary_clauses.push_back(mapped_l);
290 }
291 if (result.status == SatSolver::LIMIT_REACHED) {
292 break;
293 }
294 }
295 global_time_limit_->AdvanceDeterministicTime(
296 sweep_time_limit.GetElapsedDeterministicTime());
297 if (binary_clauses.empty() && unary_clauses.empty()) {
298 return true;
299 }
300 // TODO(user): find out why this is necessary.
301 clause_manager_->DetachAllClauses();
302 for (const auto& [l1, l2] : binary_clauses) {
303 if (!implication_graph_->AddBinaryClause(l1, l2)) return false;
304 }
305 for (const Literal l : unary_clauses) {
306 if (!implication_graph_->FixLiteral(l, {})) return false;
307 }
308 return true;
309}
310
312 const CompactVectorVector<int, Literal>& clauses, TimeLimit* time_limit,
313 std::function<void(Model*)> configure_model_before_first_solve) {
314 WallTimer wall_timer;
315 wall_timer.Start();
316 Model neighborhood_model;
317
318 TimeLimit* model_time_limit = neighborhood_model.GetOrCreate<TimeLimit>();
319 absl::Cleanup update_elapsed_dtime =
320 [model_time_limit, time_limit,
321 time_limit_dtime_start =
322 model_time_limit->GetElapsedDeterministicTime()] {
323 time_limit->AdvanceDeterministicTime(
324 model_time_limit->GetElapsedDeterministicTime() -
325 time_limit_dtime_start);
326 };
327
328 model_time_limit->MergeWithGlobalTimeLimit(time_limit);
329
330 // This algorithm splits the partitions much faster if it sees a more
331 // diversified set of solutions. So we tweak the SAT solver to do assignments
332 // more randomly.
333 SatParameters* params = neighborhood_model.GetOrCreate<SatParameters>();
336 params->set_random_polarity_ratio(0.3);
337 params->set_random_branches_ratio(0.3);
338
339 SatDecisionPolicy* decision_policy =
340 neighborhood_model.GetOrCreate<SatDecisionPolicy>();
341
342 auto* sat_solver = neighborhood_model.GetOrCreate<SatSolver>();
343 BooleanVariable max_boolean = BooleanVariable(0);
344 for (int i = 0; i < clauses.size(); ++i) {
345 for (const Literal l : clauses[i]) {
346 max_boolean = std::max(max_boolean, l.Variable());
347 }
348 }
349 CHECK_EQ(sat_solver->NumVariables(), 0);
350 sat_solver->SetNumVariables(max_boolean.value() + 1);
351
352 for (int i = 0; i < clauses.size(); ++i) {
353 sat_solver->AddProblemClause(clauses[i]);
354 }
355 configure_model_before_first_solve(&neighborhood_model);
356
357 SatSweepingResult result;
358 SatSolver* nh_solver = neighborhood_model.GetOrCreate<SatSolver>();
359 if (!nh_solver->FinishPropagation()) {
361 return result;
362 }
363
364 // We start by finding a first solution to our problem. This will be used for
365 // initializing the set of potential backbone (ie., fixable) literals and
366 // the partitions of potentially equivalent literals.
367 result.status = nh_solver->Solve();
368 if (result.status == SatSolver::INFEASIBLE ||
370 return result;
371 }
372 CHECK_EQ(result.status, SatSolver::FEASIBLE);
373
374 ModelRandomGenerator* random =
375 neighborhood_model.GetOrCreate<ModelRandomGenerator>();
376 int num_sat_calls = 1;
377 std::vector<Literal> possible_backbone;
378 const int num_variables = nh_solver->NumVariables();
379 possible_backbone.reserve(num_variables);
380 for (BooleanVariable var{0}; var < num_variables; ++var) {
381 possible_backbone.push_back(
383 }
384 std::vector<std::vector<Literal>> partitions = {possible_backbone};
385 while (!possible_backbone.empty()) {
386 // Pick a random literal from the possible backbone and try to prove it is
387 // indeed in the backbone. As a side-effect, if it is not, we get a new,
388 // different solution.
389 const int index = absl::Uniform<int>(*random, 0, possible_backbone.size());
390 const Literal l = possible_backbone[index];
391 std::swap(possible_backbone[index], possible_backbone.back());
392 possible_backbone.pop_back();
393 decision_policy->ResetDecisionHeuristic();
394 const SatSolver::Status status =
396 ++num_sat_calls;
397 if (status == SatSolver::LIMIT_REACHED) {
398 result.status = status;
399 return result;
400 }
401 if (status == SatSolver::ASSUMPTIONS_UNSAT) {
402 // Our subproblem is unsat with ~l!
403 result.unary_clauses.push_back(l);
404 // TODO(user): make sure that adding the assumption back to the
405 // model is redundant, since it could be a side-effect of returning
406 // ASSUMPTIONS_UNSAT.
407 CHECK(nh_solver->ResetToLevelZero());
408 CHECK(nh_solver->AddUnitClause(l));
409
410 // Remove from the partitions.
411 for (std::vector<Literal>& partition : partitions) {
412 int new_partition_size = 0;
413 for (int i = 0; i < partition.size(); i++) {
414 const Literal literal = partition[i];
415 if (literal == l || literal == l.Negated()) continue;
416 partition[new_partition_size++] = literal;
417 }
418 partition.resize(new_partition_size);
419 }
420 } else {
421 // This is the most common case, where the literal is not in the backbone.
422 // So we use the solution we got to refine the partitions and update the
423 // backbone.
424 CHECK(status == SatSolver::FEASIBLE);
425 // Update the backbone
426 int new_possible_backbone_size = 0;
427 for (int i = 0; i < possible_backbone.size(); ++i) {
428 if (!nh_solver->Assignment().LiteralIsTrue(possible_backbone[i])) {
429 continue;
430 }
431 // If a literal has a different polarity in this solution than it had in
432 // the previous ones, we know it's not part of the backbone.
433 possible_backbone[new_possible_backbone_size++] = possible_backbone[i];
434 }
435 possible_backbone.resize(new_possible_backbone_size);
436
437 // Use the new solution to update the partitions.
438 RefinePartitions(partitions, nh_solver->Assignment());
439 }
440 }
441 const int num_partitions = partitions.size();
442 std::vector<std::pair<Literal, Literal>> equivalences;
443 int num_equivalences = 0;
444 while (!partitions.empty()) {
445 std::vector<Literal>& partition = partitions.back();
446 if (partition.size() <= 1) {
447 partitions.pop_back();
448 continue;
449 }
450 const Literal l1 = partition[0];
451 const Literal l2 = partition.back();
452 SatSolver::Status status =
453 nh_solver->ResetAndSolveWithGivenAssumptions({l1, l2.Negated()});
454 ++num_sat_calls;
455 if (status == SatSolver::ASSUMPTIONS_UNSAT) {
456 // We found a binary clause! Add the clause (~l1, l2) to the main problem.
457 CHECK(nh_solver->ResetToLevelZero());
458 CHECK(nh_solver->AddBinaryClause(l1.Negated(), l2));
459
460 ++num_sat_calls;
461 // Now check if we have an equivalence with l1 and l2.
462 status = nh_solver->ResetAndSolveWithGivenAssumptions({l1.Negated(), l2});
463 }
464 if (status == SatSolver::LIMIT_REACHED) {
465 result.status = status;
466 return result;
467 }
468 if (status == SatSolver::ASSUMPTIONS_UNSAT) {
469 // We have an equivalence! Add it to the main problem.
470 ++num_equivalences;
471 equivalences.push_back({l1, l2});
472 partition.pop_back(); // Remove l2 from the partition. It's equivalent to
473 // l1, so it's not useful for finding more
474 // equivalences.
475 CHECK(nh_solver->ResetToLevelZero());
476 CHECK(nh_solver->AddBinaryClause(l1, l2.Negated()));
477 } else {
478 CHECK_EQ(status, SatSolver::FEASIBLE);
479 // Use the new solution to update the partitions. Note that this will
480 // at least break the current partition in two, since we now have a
481 // solution where l1 and l2 have different polarities. This guarantees
482 // that this loop will run at most num_variables times.
483 RefinePartitions(partitions, nh_solver->Assignment());
484 }
485 }
486
487 CHECK(nh_solver->ResetToLevelZero());
488 BinaryImplicationGraph* implication_graph =
489 neighborhood_model.GetOrCreate<BinaryImplicationGraph>();
490 CHECK(implication_graph->DetectEquivalences());
491
492 struct GetBinaryClause {
493 explicit GetBinaryClause(std::vector<std::pair<Literal, Literal>>& clauses)
494 : binary_clauses(clauses) {}
495 void AddBinaryClause(Literal a, Literal b) {
496 binary_clauses.push_back({a, b});
497 }
498 std::vector<std::pair<Literal, Literal>>& binary_clauses;
499 };
500
501 GetBinaryClause helper(result.binary_clauses);
502 implication_graph->ExtractAllBinaryClauses(&helper);
503
504 if (DEBUG_MODE) {
505 // Since we kept the set of all possible partitions and ran the algorithm
506 // until they were all unitary, we must have seen all possible equivalences
507 // that are valid. Check that the solver didn't found more equivalences than
508 // we did.
509 int num_equivalences_in_model = 0;
510 for (BooleanVariable var{0}; var < num_variables; ++var) {
511 const Literal l = Literal(var, true);
512 num_equivalences_in_model += implication_graph->RepresentativeOf(l) != l;
513 }
514 DCHECK_EQ(num_equivalences_in_model, num_equivalences);
515 }
516
517 // Remove binary clauses that were already in the input
518 absl::flat_hash_set<std::pair<Literal, Literal>> input_clauses;
519 for (int i = 0; i < clauses.size(); i++) {
520 const absl::Span<const Literal> clause = clauses[i];
521 if (clause.size() != 2) continue;
522 Literal l1 = clause[0];
523 Literal l2 = clause[1];
524 if (l1 < l2) std::swap(l1, l2);
525 input_clauses.insert({l1, l2});
526 }
527 int new_binary_clauses_size = 0;
528 for (int i = 0; i < result.binary_clauses.size(); ++i) {
529 std::pair<Literal, Literal>& clause = result.binary_clauses[i];
530 if (clause.first < clause.second) {
531 std::swap(clause.first, clause.second);
532 }
533 if (input_clauses.contains(clause)) continue;
534
535 result.binary_clauses[new_binary_clauses_size++] = clause;
536 }
537 result.binary_clauses.resize(new_binary_clauses_size);
538
539 VLOG(2) << "num_booleans: " << num_variables
540 << " num_clauses: " << clauses.size()
541 << " num_partitions: " << num_partitions
542 << " num_unary_clauses: " << result.unary_clauses.size()
543 << " num_binary_clauses: " << result.binary_clauses.size()
544 << " num_equivalences: " << num_equivalences
545 << " num_sat_calls: " << num_sat_calls
546 << " dtime: " << model_time_limit->GetElapsedDeterministicTime()
547 << " wtime: " << wall_timer.Get();
548
549 return result;
550}
551
552} // namespace sat
553} // namespace operations_research
double Get() const
Definition timer.h:44
void Start()
Definition timer.h:30
void MergeWithGlobalTimeLimit(const TimeLimit *other)
Definition time_limit.h:514
double GetElapsedDeterministicTime() const
Definition time_limit.h:220
void ChangeDeterministicLimit(double new_limit)
Definition time_limit.h:275
Literal RepresentativeOf(Literal l) const
Definition clause.h:765
void ExtractAllBinaryClauses(Output *out) const
Definition clause.h:889
bool DetectEquivalences(bool log_info=false)
Definition clause.cc:1950
void AppendToLastVector(const V &value)
Definition util.h:944
int Add(absl::Span< const V > values)
Definition util.h:935
bool DoOneRound(std::function< void(Model *)> run_inprocessing)
BooleanVariable Variable() const
Definition sat_base.h:88
void set_preferred_variable_order(::operations_research::sat::SatParameters_VariableOrder value)
void set_initial_polarity(::operations_research::sat::SatParameters_Polarity value)
static constexpr VariableOrder IN_RANDOM_ORDER
bool AddBinaryClause(Literal a, Literal b)
ABSL_MUST_USE_RESULT bool FinishPropagation(std::optional< ConflictCallback > callback=std::nullopt)
ABSL_MUST_USE_RESULT bool AddUnitClause(Literal true_literal)
Status ResetAndSolveWithGivenAssumptions(const std::vector< Literal > &assumptions, int64_t max_number_of_conflicts=-1)
const VariablesAssignment & Assignment() const
Definition sat_solver.h:420
ABSL_MUST_USE_RESULT bool ResetToLevelZero()
Literal GetTrueLiteralForAssignedVariable(BooleanVariable var) const
Definition sat_base.h:221
bool LiteralIsTrue(Literal literal) const
Definition sat_base.h:206
SatSweepingResult DoFullSatSweeping(const CompactVectorVector< int, Literal > &clauses, TimeLimit *time_limit, std::function< void(Model *)> configure_model_before_first_solve)
OR-Tools root namespace.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
const bool DEBUG_MODE
Definition radix_sort.h:266
std::vector< std::pair< Literal, Literal > > binary_clauses