// Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ortools/sat/clause.h" #include #include #include #include #include #include "absl/container/flat_hash_set.h" #include "absl/random/random.h" #include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_base.h" #include "ortools/sat/sat_solver.h" #include "ortools/util/strong_integers.h" namespace operations_research { namespace sat { namespace { using ::testing::ElementsAre; template auto LiteralsAre(Args... literals) { return ::testing::ElementsAre(Literal(literals)...); } template auto UnorderedLiteralsAre(Args... literals) { return ::testing::UnorderedElementsAre(Literal(literals)...); } TEST(SatClauseTest, BasicAllocation) { std::unique_ptr clause(SatClause::Create(Literals({+1, -2, +4}))); EXPECT_EQ(3, clause->size()); EXPECT_EQ(Literal(+1), clause->FirstLiteral()); EXPECT_EQ(Literal(-2), clause->SecondLiteral()); } struct TestSatClause { #ifdef _MSC_VER // MSVC doesn't allow to overflow at the word boundary. unsigned int is_learned : 1; unsigned int is_attached : 1; #else bool is_learned : 1; bool is_attached : 1; #endif unsigned int size : 30; // We test that Literal literals[0]; does not increase the size. }; TEST(SatClauseTest, ClassSize) { EXPECT_EQ(4, sizeof(TestSatClause)); EXPECT_EQ(4, sizeof(SatClause)); } BinaryClause MakeBinaryClause(int a, int b) { return BinaryClause(Literal(a), Literal(b)); } TEST(BinaryClauseManagerTest, BasicTest) { BinaryClauseManager manager; manager.Add(MakeBinaryClause(+2, +3)); manager.Add(MakeBinaryClause(+3, +2)); // dup manager.Add(MakeBinaryClause(+1, +4)); manager.Add(MakeBinaryClause(+5, +4)); manager.Add(MakeBinaryClause(+4, +1)); // dup manager.Add(MakeBinaryClause(+2, +3)); // dup EXPECT_EQ(3, manager.NumClauses()); EXPECT_THAT(manager.newly_added(), ElementsAre(MakeBinaryClause(+2, +3), MakeBinaryClause(+1, +4), MakeBinaryClause(+5, +4))); manager.ClearNewlyAdded(); EXPECT_TRUE(manager.newly_added().empty()); manager.Add(MakeBinaryClause(-1, +2)); EXPECT_EQ(4, manager.NumClauses()); EXPECT_THAT(manager.newly_added(), ElementsAre(MakeBinaryClause(-1, +2))); } TEST(BinaryImplicationGraphTest, BasicUnsatSccTest) { Model model; model.GetOrCreate()->Resize(10); auto* graph = model.GetOrCreate(); graph->Resize(10); // These are implications. graph->AddBinaryClause(Literal(+1).Negated(), Literal(+2)); graph->AddBinaryClause(Literal(+2).Negated(), Literal(+3)); graph->AddBinaryClause(Literal(+3).Negated(), Literal(-1)); graph->AddBinaryClause(Literal(-1).Negated(), Literal(+4)); graph->AddBinaryClause(Literal(+4).Negated(), Literal(+1)); EXPECT_FALSE(graph->DetectEquivalences()); } TEST(BinaryImplicationGraphTest, IssueFoundOnMipLibTest) { Model model; auto* graph = model.GetOrCreate(); model.GetOrCreate()->SetNumVariables(10); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, -3, -4}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +4, +5}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, -2, -3, +4, +5}))); EXPECT_TRUE(graph->DetectEquivalences()); } TEST(BinaryImplicationGraphTest, DetectEquivalences) { // We take a bunch of random permutations, equivalence classes will be cycles. // We make sure the representative of x and not(x) are always negation of // each other. absl::BitGen random; for (int num_passes = 0; num_passes < 10; ++num_passes) { Model model; auto* graph = model.GetOrCreate(); const int size = 1000; model.GetOrCreate()->SetNumVariables(size); std::vector permutation(size); std::iota(permutation.begin(), permutation.end(), 0); std::shuffle(permutation.begin(), permutation.end(), random); for (int i = 0; i < size; ++i) { // i => permutation[i]. const int signed_value = i + 1; const int signed_image = permutation[i] + 1; graph->AddBinaryClause(Literal(signed_value).Negated(), Literal(signed_image)); } EXPECT_TRUE(graph->DetectEquivalences()); int num_classes = 0; for (int i = 0; i < size; ++i) { const int signed_value = i + 1; if (graph->RepresentativeOf(Literal(signed_value)) == Literal(signed_value)) { ++num_classes; } else { EXPECT_EQ(graph->RepresentativeOf(Literal(signed_value)).Negated(), graph->RepresentativeOf(Literal(signed_value).Negated())); } } // It is unlikely that std::shuffle() produce the identity permutation, so // this is not flaky and shows that there is some detection going on. EXPECT_GT(num_classes, 0); EXPECT_LT(num_classes, size); } } TEST(BinaryImplicationGraphTest, DetectEquivalencesWithAtMostOnes) { Model model; auto* graph = model.GetOrCreate(); model.GetOrCreate()->SetNumVariables(10); // 2 and 4 are equivalent. They actually must be true in this setting. EXPECT_TRUE(graph->AddAtMostOne(Literals({1, 2, 3}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({1, 4, 3}))); graph->AddBinaryClause(Literal(1), Literal(4)); graph->AddBinaryClause(Literal(3), Literal(2)); const auto& assignment = model.GetOrCreate()->Assignment(); EXPECT_TRUE(graph->DetectEquivalences()); EXPECT_TRUE(assignment.LiteralIsTrue(Literal(2))); EXPECT_TRUE(assignment.LiteralIsTrue(Literal(4))); EXPECT_TRUE(assignment.LiteralIsFalse(Literal(1))); EXPECT_TRUE(assignment.LiteralIsFalse(Literal(3))); } TEST(BinaryImplicationGraphTest, TransitiveReduction) { Model model; model.GetOrCreate()->SetNumVariables(10); auto* graph = model.GetOrCreate(); for (BooleanVariable i(0); i < 10; ++i) { for (BooleanVariable j(i + 1); j < 10; ++j) { // i => j graph->AddBinaryClause(Literal(i, false), Literal(j, true)); } // These trivial clauses are filtered. graph->AddBinaryClause(Literal(i, false), Literal(i, true)); } EXPECT_EQ(graph->ComputeNumImplicationsForLog(), 10 * 9); EXPECT_TRUE(graph->ComputeTransitiveReduction()); EXPECT_EQ(graph->ComputeNumImplicationsForLog(), 9 * 2); } // This basically just test our DCHECKs. TEST(BinaryImplicationGraphTest, BasicRandomTransitiveReduction) { Model model; const int num_vars = 200; model.GetOrCreate()->SetNumVariables(num_vars); auto* graph = model.GetOrCreate(); // We add a lot of a => b (we might not have a DAG). absl::BitGen random; int num_added = 0; for (int i = 0; i < 10'000; ++i) { const BooleanVariable a(absl::Uniform(random, 0, num_vars)); const BooleanVariable b(absl::Uniform(random, 0, num_vars)); if (a == b) continue; ++num_added; graph->AddImplication(Literal(a, true), Literal(b, true)); } EXPECT_EQ(graph->ComputeNumImplicationsForLog(), 2 * num_added); EXPECT_TRUE(graph->ComputeTransitiveReduction()); EXPECT_LT(graph->ComputeNumImplicationsForLog(), num_added); } // We generate a random 2-SAT problem, and check that the propagation is // unchanged whether or not the graph is reduced. TEST(BinaryImplicationGraph, RandomTransitiveReduction) { // These leads to a not trivial 2-SAT space with more than just all zero // and all 1 as solution. const int num_variables = 100; const int num_constraints = 200; Model model1; Model model2; auto* sat1 = model1.GetOrCreate(); auto* sat2 = model2.GetOrCreate(); sat1->SetNumVariables(num_variables); sat2->SetNumVariables(num_variables); absl::BitGen random; for (int i = 0; i < num_constraints; ++i) { // Because we only use positive literal, we are never UNSAT. const Literal a = Literal(BooleanVariable(absl::Uniform(random, 0, num_variables)), true); const Literal b = Literal(BooleanVariable(absl::Uniform(random, 0, num_variables)), true); // a => b. sat1->AddBinaryClause(a.Negated(), b); sat2->AddBinaryClause(a.Negated(), b); sat2->AddBinaryClause(a.Negated(), b); } auto* graph2 = model2.GetOrCreate(); EXPECT_TRUE(graph2->ComputeTransitiveReduction()); EXPECT_TRUE(sat1->Propagate()); EXPECT_TRUE(sat2->Propagate()); absl::flat_hash_set propagated; for (BooleanVariable var(0); var < num_variables; ++var) { sat1->Backtrack(0); sat2->Backtrack(0); sat1->EnqueueDecisionIfNotConflicting(Literal(var, true)); sat2->EnqueueDecisionIfNotConflicting(Literal(var, true)); EXPECT_EQ(sat1->LiteralTrail().Index(), sat2->LiteralTrail().Index()); propagated.clear(); for (int i = 0; i < sat1->LiteralTrail().Index(); ++i) { propagated.insert(sat1->LiteralTrail()[i].Index()); } for (int i = 0; i < sat2->LiteralTrail().Index(); ++i) { EXPECT_TRUE(propagated.contains(sat2->LiteralTrail()[i].Index())); } } } TEST(BinaryImplicationGraphTest, BasicCliqueDetection) { std::vector> at_most_ones; at_most_ones.push_back({Literal(+1), Literal(+2)}); at_most_ones.push_back({Literal(+1), Literal(+3)}); at_most_ones.push_back({Literal(+2), Literal(+3)}); Model model; auto* graph = model.GetOrCreate(); graph->Resize(10); model.GetOrCreate()->Resize(10); for (const std::vector& at_most_one : at_most_ones) { EXPECT_TRUE(graph->AddAtMostOne(at_most_one)); } graph->TransformIntoMaxCliques(&at_most_ones); EXPECT_THAT(at_most_ones[0], LiteralsAre(+1, +2, +3)); EXPECT_TRUE(at_most_ones[1].empty()); EXPECT_TRUE(at_most_ones[2].empty()); } TEST(BinaryImplicationGraphTest, CliqueDetectionAndDuplicates) { std::vector> at_most_ones; at_most_ones.push_back({Literal(+1), Literal(+2)}); at_most_ones.push_back({Literal(+2), Literal(+2)}); Model model; auto* graph = model.GetOrCreate(); model.GetOrCreate()->SetNumVariables(10); for (const std::vector& at_most_one : at_most_ones) { EXPECT_TRUE(graph->AddAtMostOne(at_most_one)); } // Here we do not change the clique. graph->TransformIntoMaxCliques(&at_most_ones); EXPECT_THAT(at_most_ones, ElementsAre(LiteralsAre(+1, +2), LiteralsAre(+2, +2))); // Clique detection call the SCC which will see that 2 must be false... const auto& assignment = model.GetOrCreate()->Assignment(); EXPECT_FALSE(assignment.LiteralIsAssigned(Literal(1))); EXPECT_TRUE(assignment.LiteralIsFalse(Literal(2))); } TEST(BinaryImplicationGraphTest, AddAtMostOneWithDuplicates) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(10); graph->Resize(10); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +2}))); EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+2))); } TEST(BinaryImplicationGraphTest, AddAtMostOneWithTriples) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(10); graph->Resize(10); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +2, +2}))); EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+2))); } TEST(BinaryImplicationGraphTest, AddAtMostOneCornerCase) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(10); graph->Resize(10); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +2, -2}))); EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+1))); EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+2))); EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+3))); } TEST(BinaryImplicationGraphTest, LargeAtMostOnePropagation) { const int kNumVariables = 1e6; Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(kNumVariables); graph->Resize(kNumVariables); std::vector large_at_most_one; for (int i = 0; i < kNumVariables; ++i) { large_at_most_one.push_back(Literal(BooleanVariable(i), true)); } EXPECT_TRUE(graph->AddAtMostOne(large_at_most_one)); const Literal decision = Literal(BooleanVariable(42), true); trail->EnqueueSearchDecision(Literal(decision)); EXPECT_TRUE(graph->Propagate(trail)); const auto& assignment = trail->Assignment(); for (int i = 0; i < kNumVariables; ++i) { const Literal l = Literal(BooleanVariable(i), true); if (i == 42) { EXPECT_TRUE(assignment.LiteralIsTrue(l)); } else { EXPECT_TRUE(assignment.LiteralIsFalse(l)); EXPECT_EQ(trail->Reason(l.Variable()), absl::Span({decision.Negated()})); } } } TEST(BinaryImplicationGraphTest, HeuristicAmoPartition) { const int kNumVariables = 1e6; Model model; auto* trail = model.GetOrCreate(); model.GetOrCreate()->set_at_most_one_max_expansion_size(2); auto* graph = model.GetOrCreate(); trail->Resize(kNumVariables); graph->Resize(kNumVariables); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +4}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({+4, +5, +6, +7}))); std::vector literals = Literals({+1, +2, +5, +6, +7}); EXPECT_THAT(graph->HeuristicAmoPartition(&literals), ElementsAre(UnorderedLiteralsAre(+5, +6, +7), UnorderedLiteralsAre(+1, +2))); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +6, +7}))); EXPECT_THAT(graph->HeuristicAmoPartition(&literals), ElementsAre(LiteralsAre(+6, +7, +2, +1))); } TEST(BinaryImplicationGraphTest, RandomImpliedLiteral) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(100); graph->Resize(100); graph->AddImplication(Literal(+1), Literal(+6)); graph->AddImplication(Literal(+1), Literal(+7)); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +4, +5}))); absl::flat_hash_set seen; for (int i = 0; i < 100; ++i) { seen.insert(graph->RandomImpliedLiteral(Literal(+1))); } EXPECT_THAT(seen, UnorderedLiteralsAre(-2, -4, -5, +6, +7)); } TEST(BinaryImplicationGraphTest, DetectEquivalencePropagateThings) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(10); graph->Resize(10); EXPECT_TRUE(graph->AddAtMostOne(Literals({+1, +2, +3, +4}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({-2, -1, +3, +4}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({-4, -1, +2, +3}))); EXPECT_TRUE(graph->AddAtMostOne(Literals({-3, -1, +2, +4}))); EXPECT_TRUE(graph->DetectEquivalences()); } void TryAmoEquivalences(absl::Span> cliques) { Model model; auto* trail = model.GetOrCreate(); auto* graph = model.GetOrCreate(); trail->Resize(1000); graph->Resize(1000); for (const auto& clique : cliques) { std::vector literals; for (const int i : clique) { literals.push_back(Literal(i)); } if (!graph->AddAtMostOne(literals)) { return; } } graph->DetectEquivalences(); } } // namespace } // namespace sat } // namespace operations_research