// 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/synchronization.h" #include #include #include #include #include #include #include "absl/time/time.h" #include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/parse_test_proto.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" #include "ortools/sat/util.h" #include "ortools/util/random_engine.h" namespace operations_research { namespace sat { namespace { using ::google::protobuf::contrib::parse_proto::ParseTestProto; using ::testing::ElementsAre; using ::testing::EqualsProto; using ::testing::IsEmpty; TEST(SharedSolutionRepository, Api) { SharedSolutionRepository repository(3); EXPECT_EQ(repository.NumSolutions(), 0); repository.Add({8, {1, 2}}); repository.Add({1, {2, 3}}); EXPECT_EQ(repository.NumSolutions(), 0); repository.Synchronize(); EXPECT_EQ(repository.NumSolutions(), 2); EXPECT_EQ(repository.GetSolution(0)->rank, 1); EXPECT_EQ(repository.GetSolution(1)->rank, 8); EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, /*solution_index=*/0), 3); EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/0, /*solution_index=*/1), 1); repository.Add({3, {1, 2}}); repository.Add({5, {1, 2}}); repository.Add({1, {2, 3}}); repository.Add({9, {1, 2}}); EXPECT_EQ(repository.NumSolutions(), 2); repository.Synchronize(); EXPECT_EQ(repository.NumSolutions(), 3); EXPECT_EQ(repository.GetSolution(0)->rank, 1); EXPECT_EQ(repository.GetSolution(1)->rank, 3); EXPECT_EQ(repository.GetSolution(2)->rank, 5); EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, /*solution_index=*/0), 3); EXPECT_EQ(repository.GetVariableValueInSolution(/*var_index=*/1, /*solution_index=*/1), 2); } TEST(SharedSolutionRepository, DuplicateSolutionAreMerged) { SharedSolutionRepository repository(3); EXPECT_EQ(repository.NumSolutions(), 0); repository.Add({1, {1, 50}}); // In practice we shouldn't have the same variable values and different // objective, but the code don't care about this and just test the perfect // equality. repository.Add({5, {1, 50}}); repository.Add({1, {1, 50}}); EXPECT_EQ(repository.NumSolutions(), 0); repository.Synchronize(); EXPECT_EQ(repository.NumSolutions(), 2); EXPECT_EQ(repository.GetSolution(0)->rank, 1); EXPECT_EQ(repository.GetSolution(1)->rank, 5); } TEST(SharedSolutionRepository, DuplicateSolutionAreMerged2) { SharedSolutionRepository repository(3); EXPECT_EQ(repository.NumSolutions(), 0); // All this should count as 1 solution. repository.Add({3, {1, 50}}); repository.Add({3, {1, 50}}); repository.Add({3, {1, 50}}); repository.Add({3, {1, 50}}); repository.Add({3, {1, 50}}); // So we should be able to Enqueue worse ones. repository.Add({5, {1, 50}}); repository.Add({6, {1, 50}}); repository.Synchronize(); EXPECT_EQ(repository.NumSolutions(), 3); EXPECT_EQ(repository.GetSolution(0)->rank, 3); EXPECT_EQ(repository.GetSolution(1)->rank, 5); EXPECT_EQ(repository.GetSolution(2)->rank, 6); } TEST(SharedSolutionRepository, GetRandomBiasedSolution) { SharedSolutionRepository repository(5); EXPECT_EQ(repository.NumSolutions(), 0); repository.Add({3, {1, 50}}); repository.Add({3, {1, 51}}); repository.Add({3, {1, 52}}); // Enqueue worse ones. repository.Add({5, {1, 50}}); repository.Add({6, {1, 50}}); repository.Synchronize(); EXPECT_EQ(repository.NumSolutions(), 5); // We select one of the solution with best objective. random_engine_t random(0); for (int i = 0; i < 10; ++i) { EXPECT_EQ(repository.GetRandomBiasedSolution(random)->rank, 3); } } TEST(SharedLPSolutionRepository, NewLPSolution) { SharedLPSolutionRepository lp_solutions(1); lp_solutions.NewLPSolution({1.0, 2.0, 1.0}); lp_solutions.Synchronize(); EXPECT_EQ(lp_solutions.NumSolutions(), 1); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[0], 1.0); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[1], 2.0); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[2], 1.0); lp_solutions.NewLPSolution({2.0, 3.0, 0.0}); lp_solutions.Synchronize(); EXPECT_EQ(lp_solutions.NumSolutions(), 1); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[0], 2.0); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[1], 3.0); EXPECT_EQ(lp_solutions.GetSolution(0)->variable_values[2], 0.0); } TEST(SharedIncompleteSolutionManager, AddAndRemoveSolutions) { SharedIncompleteSolutionManager incomplete_solutions; EXPECT_FALSE(incomplete_solutions.HasSolution()); incomplete_solutions.AddSolution({1.0, 2.0, 1.0}); EXPECT_TRUE(incomplete_solutions.HasSolution()); std::vector solution = incomplete_solutions.PopLast(); EXPECT_THAT(solution, ::testing::ElementsAre(1.0, 2.0, 1.0)); EXPECT_FALSE(incomplete_solutions.HasSolution()); incomplete_solutions.AddSolution({2.0, 3.0, 2.0}); incomplete_solutions.AddSolution({3.0, 4.0, 3.0}); EXPECT_TRUE(incomplete_solutions.HasSolution()); solution = incomplete_solutions.PopLast(); EXPECT_THAT(solution, ::testing::ElementsAre(3.0, 4.0, 3.0)); EXPECT_TRUE(incomplete_solutions.HasSolution()); solution = incomplete_solutions.PopLast(); EXPECT_THAT(solution, ::testing::ElementsAre(2.0, 3.0, 2.0)); EXPECT_FALSE(incomplete_solutions.HasSolution()); } TEST(SharedBoundsManagerTest, Api) { const CpModelProto model = ParseTestProto(R"pb( variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } )pb"); SharedBoundsManager manager(model); manager.ReportPotentialNewBounds("auto", {2, 4, 6}, {1, 2, 3}, {11, 12, 13}); manager.Synchronize(); std::vector vars; std::vector lbs; std::vector ubs; EXPECT_EQ(manager.RegisterNewId(), 0); manager.GetChangedBounds(0, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(2, 4)); EXPECT_THAT(lbs, ElementsAre(1, 2)); EXPECT_THAT(ubs, ElementsAre(11, 12)); EXPECT_EQ(manager.RegisterNewId(), 1); manager.GetChangedBounds(1, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(2, 4)); EXPECT_THAT(lbs, ElementsAre(1, 2)); EXPECT_THAT(ubs, ElementsAre(11, 12)); EXPECT_EQ(manager.RegisterNewId(), 2); manager.GetChangedBounds(2, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(2, 4)); EXPECT_THAT(lbs, ElementsAre(1, 2)); EXPECT_THAT(ubs, ElementsAre(11, 12)); // Test nilpotence. manager.GetChangedBounds(2, &vars, &lbs, &ubs); EXPECT_THAT(vars, IsEmpty()); EXPECT_THAT(lbs, IsEmpty()); EXPECT_THAT(ubs, IsEmpty()); // Non improving bounds, and partially improving bounds. manager.ReportPotentialNewBounds("fixed", {2, 4}, {0, 5}, {20, 20}); manager.Synchronize(); manager.GetChangedBounds(0, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(4)); EXPECT_THAT(lbs, ElementsAre(5)); EXPECT_THAT(ubs, ElementsAre(12)); manager.GetChangedBounds(1, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(4)); EXPECT_THAT(lbs, ElementsAre(5)); EXPECT_THAT(ubs, ElementsAre(12)); manager.GetChangedBounds(2, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(4)); EXPECT_THAT(lbs, ElementsAre(5)); EXPECT_THAT(ubs, ElementsAre(12)); // Matching bounds. manager.ReportPotentialNewBounds("fixed", {2}, {1}, {11}); manager.Synchronize(); manager.GetChangedBounds(0, &vars, &lbs, &ubs); EXPECT_THAT(vars, IsEmpty()); EXPECT_THAT(lbs, IsEmpty()); EXPECT_THAT(ubs, IsEmpty()); manager.GetChangedBounds(1, &vars, &lbs, &ubs); EXPECT_THAT(vars, IsEmpty()); EXPECT_THAT(lbs, IsEmpty()); EXPECT_THAT(ubs, IsEmpty()); manager.GetChangedBounds(2, &vars, &lbs, &ubs); EXPECT_THAT(vars, IsEmpty()); EXPECT_THAT(lbs, IsEmpty()); EXPECT_THAT(ubs, IsEmpty()); } TEST(SharedBoundsManagerTest, WithSymmetry) { const CpModelProto model = ParseTestProto(R"pb( variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } variables { domain: [ 0, 20 ] } symmetry { permutations { support: [ 0, 1, 2 ] cycle_sizes: [ 3 ] } } )pb"); SharedBoundsManager manager(model); manager.ReportPotentialNewBounds("auto", /*variables=*/{2, 1}, {4, 2}, {7, 9}); manager.Synchronize(); std::vector vars; std::vector lbs; std::vector ubs; EXPECT_EQ(manager.RegisterNewId(), 0); manager.GetChangedBounds(0, &vars, &lbs, &ubs); EXPECT_THAT(vars, ElementsAre(0, 1, 2)); EXPECT_THAT(lbs, ElementsAre(4, 4, 4)); EXPECT_THAT(ubs, ElementsAre(7, 7, 7)); } TEST(SharedResponseManagerTest, InitialResponseSAT) { Model model; auto* shared_response = model.GetOrCreate(); EXPECT_EQ(0.0, shared_response->GapIntegral()); const CpSolverResponse response = ParseTestProto(R"pb( status: UNKNOWN, )pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); } TEST(SharedResponseManagerTest, InitialResponseMinimization) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: 1 })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); const CpSolverResponse response = ParseTestProto(R"pb( status: UNKNOWN, objective_value: inf, best_objective_bound: -inf, inner_objective_lower_bound: -9223372036854775808, )pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); } TEST(SharedResponseManagerTest, InitialResponseMaximization) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: -1 })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); const CpSolverResponse response = ParseTestProto(R"pb( status: UNKNOWN, objective_value: -inf, best_objective_bound: inf, inner_objective_lower_bound: -9223372036854775808, )pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); } TEST(SharedResponseManagerTest, UnknownResponseMinimization) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: 0 })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), IntegerValue(7)); shared_response->UpdateGapIntegral(); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateGapIntegral(); EXPECT_EQ(1.0 * std::log(1 + 11), shared_response->GapIntegral()); const std::string response = R"pb( objective_value: 7, best_objective_bound: -4, inner_objective_lower_bound: -4 )pb"; auto result = shared_response->GetResponse(); EXPECT_EQ(result.objective_value(), 7); EXPECT_EQ(result.best_objective_bound(), -4); EXPECT_EQ(result.inner_objective_lower_bound(), -4); EXPECT_EQ(result.status(), UNKNOWN); } TEST(SharedResponseManagerTest, UnknownResponseMaximization) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: -4 })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), IntegerValue(7)); shared_response->UpdateGapIntegral(); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateGapIntegral(); EXPECT_EQ(1.0 * log(1 + 4.0 * 11), shared_response->GapIntegral()); const std::string response = R"pb( objective_value: -28, best_objective_bound: 16, inner_objective_lower_bound: -4 )pb"; auto result = shared_response->GetResponse(); EXPECT_EQ(result.objective_value(), -28); EXPECT_EQ(result.best_objective_bound(), 16); EXPECT_EQ(result.inner_objective_lower_bound(), -4); EXPECT_EQ(result.status(), UNKNOWN); } TEST(SharedResponseManagerTest, GapIntegralTest) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: -4 offset: 100 })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); // At the beginning the primal integral is zero, and will start counting // on the first update, ignoring any earlier time. This leave a change to // use reasonable bound on the objective. EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateGapIntegral(); EXPECT_EQ(0.0, shared_response->GapIntegral()); // Unknown count as max possible difference. shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateGapIntegral(); const double value1 = 1.0 * log(1 + 4 * (static_cast(std::numeric_limits::max()) - static_cast(std::numeric_limits::min()))); EXPECT_EQ(value1, shared_response->GapIntegral()); // No time, so still same. But the function height will change. shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), IntegerValue(7)); shared_response->UpdateGapIntegral(); EXPECT_EQ(value1, shared_response->GapIntegral()); // Add time, increase the integral. shared_time_limit->AdvanceDeterministicTime(3.0); shared_response->UpdateGapIntegral(); EXPECT_EQ(value1 + 3.0 * log(1 + 4.0 * 11), shared_response->GapIntegral()); } TEST(SharedResponseManagerTest, GapIntegralOnEachChangeTest) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: -4 offset: 100 })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); // Starts with reasonable bound this time. shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), IntegerValue(10)); shared_response->SetUpdateGapIntegralOnEachChange(true); shared_response->UpdateGapIntegral(); EXPECT_EQ(0.0, shared_response->GapIntegral()); // First Update. shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), IntegerValue(7)); shared_response->UpdateGapIntegral(); double expected = 1.0 * log(1 + 4 * 10); EXPECT_EQ(expected, shared_response->GapIntegral()); // Updating bound with no time, do not do anything. shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), IntegerValue(3)); EXPECT_EQ(expected, shared_response->GapIntegral()); // Add time, and change bound increase the integral. shared_time_limit->AdvanceDeterministicTime(3.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), IntegerValue(2)); expected += 3.0 * log(1 + 4.0 * 3); EXPECT_EQ(expected, shared_response->GapIntegral()); // Closing the search still increase it. And we deal correcly with bound // crossing. shared_time_limit->AdvanceDeterministicTime(1.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(10), IntegerValue(0)); expected += 1.0 * log(1 + 4.0 * 2); EXPECT_EQ(expected, shared_response->GapIntegral()); } TEST(SharedResponseManagerDeathTest, UpdateInnerObjectiveBoundsOnSAT) { Model model; auto* shared_response = model.GetOrCreate(); EXPECT_DEATH(shared_response->UpdateInnerObjectiveBounds("", IntegerValue(0), IntegerValue(0)), ""); } TEST(SharedResponseManagerTest, Infeasible) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { scaling_factor: 0 })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(-4), IntegerValue(7)); shared_response->NotifyThatImprovingProblemIsInfeasible(""); const CpSolverResponse response = ParseTestProto(R"pb( status: INFEASIBLE, )pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } TEST(SharedResponseManagerTest, Solution) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(2), IntegerValue(20)); const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 2, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); { const CpSolverResponse response = ParseTestProto(R"pb( status: FEASIBLE, solution_info: "test" solution: [ 1, 2, 1 ] objective_value: 12 best_objective_bound: 2 inner_objective_lower_bound: 2)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_FALSE(shared_response->ProblemIsSolved()); } // Optimal. shared_response->NotifyThatImprovingProblemIsInfeasible(""); { const CpSolverResponse response = ParseTestProto(R"pb( status: OPTIMAL, solution_info: "test" solution: [ 1, 2, 1 ] objective_value: 12 best_objective_bound: 12 inner_objective_lower_bound: 12)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } } TEST(SharedResponseManagerTest, BestBound) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] offset: 0.5 scaling_factor: 3.0 })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); double result = 0.0; auto observer = [&result](double value) { result = value; }; shared_response->AddBestBoundCallback(observer); shared_response->UpdateInnerObjectiveBounds("", 2, 20); EXPECT_EQ(result, 7.5); } TEST(SharedResponseManagerTest, SolutionToFeasibilityProblem) { Model model; auto* shared_response = model.GetOrCreate(); const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 2, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); { const CpSolverResponse response = ParseTestProto(R"pb( status: OPTIMAL, solution_info: "test" solution: [ 1, 2, 1 ])pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } } TEST(SharedResponseManagerTest, OptimalReachedBecauseOfBounds) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(10.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(12), IntegerValue(12)); const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 2, 1 ] )pb"); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(10.0); shared_response->NewSolution(solution.solution(), solution.solution_info()); EXPECT_EQ(0.0, shared_response->GapIntegral()); const CpSolverResponse response = ParseTestProto(R"pb( status: OPTIMAL, solution_info: "test" solution: [ 1, 2, 1 ] objective_value: 12 best_objective_bound: 12 inner_objective_lower_bound: 12)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } TEST(SharedResponseManagerTest, ProblemCanBeClosedWithJustBoundUpdates) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_time_limit = model.GetOrCreate(); auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); EXPECT_EQ(0.0, shared_response->GapIntegral()); shared_time_limit->AdvanceDeterministicTime(10.0); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(13), IntegerValue(12)); EXPECT_EQ(0.0, shared_response->GapIntegral()); const CpSolverResponse response = ParseTestProto(R"pb( status: INFEASIBLE)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } TEST(SharedResponseManagerTest, ProblemCanBeClosedWithJustBoundUpdates2) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 2, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(13), IntegerValue(15)); const CpSolverResponse response = ParseTestProto(R"pb( status: OPTIMAL, solution_info: "test" solution: [ 1, 2, 1 ] objective_value: 12 best_objective_bound: 12, inner_objective_lower_bound: 12)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); EXPECT_TRUE(shared_response->ProblemIsSolved()); } #ifndef NDEBUG // TODO(user): Having a check sometime fail in multithread. Understand how // the code can push an invalid lower bound (and still be valid). The likely // behavior, is that at the end of the search, when the improving problem is // infeasible, then we might have no guarantee that while incorporating new // bounds, one thread pushes the lower bound too high ? TEST(SharedResponseManagerDeathTest, InnerBoundMustBeValid) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); shared_response->UpdateInnerObjectiveBounds("", IntegerValue(20), IntegerValue(25)); const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 2, 1 ] )pb"); // The lower bound is not globally valid! EXPECT_DEATH(shared_response->NewSolution(solution.solution(), solution.solution_info()), ""); } TEST(SharedResponseManagerDeathTest, OptimalCannotBeImproved) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); shared_response->NewSolution({1, 2, 1}, "test"); shared_response->NotifyThatImprovingProblemIsInfeasible(""); shared_response->NotifyThatImprovingProblemIsInfeasible(""); { const CpSolverResponse response = ParseTestProto(R"pb( status: OPTIMAL, solution_info: "test" solution: [ 1, 2, 1 ] objective_value: 12 best_objective_bound: 12 inner_objective_lower_bound: 12)pb"); EXPECT_THAT(shared_response->GetResponse(), EqualsProto(response)); } { const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 1, 1 ] )pb"); EXPECT_DEATH(shared_response->NewSolution(solution.solution(), solution.solution_info()), ""); } } TEST(SharedResponseManagerDeathTest, BetterSolutionMustNotArriveAfterInfeasible) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); // First solution. shared_response->NewSolution({1, 1, 1}, "test"); shared_response->NotifyThatImprovingProblemIsInfeasible(""); shared_response->NotifyThatImprovingProblemIsInfeasible(""); EXPECT_EQ(shared_response->GetResponse().status(), CpSolverStatus::OPTIMAL); { // Better solution are not possible! otherwise there is a bug. EXPECT_DEATH(shared_response->NewSolution({1, 0, 1}, "test2"), ""); } } #endif // NDEBUG TEST(SharedResponseManagerTest, Callback) { const CpModelProto model_proto = ParseTestProto(R"pb( objective: { vars: [ 0, 1, 2 ] coeffs: [ 2, 3, 4 ] })pb"); Model model; auto* shared_response = model.GetOrCreate(); shared_response->InitializeObjective(model_proto); int num_solutions = 0; const int callback_id = shared_response->AddSolutionCallback( [&num_solutions](const CpSolverResponse& /*response*/) { ++num_solutions; }); { const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 1, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); EXPECT_EQ(num_solutions, 1); } // Not improving, so not called. { const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 1, 1, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); EXPECT_EQ(num_solutions, 1); } // Improving, so called. { const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 0, 1, 1 ] )pb"); shared_response->NewSolution(solution.solution(), solution.solution_info()); EXPECT_EQ(num_solutions, 2); } // Improving, but unregistered, so not called. { const CpSolverResponse solution = ParseTestProto(R"pb( solution_info: "test" solution: [ 0, 1, 0 ] )pb"); shared_response->UnregisterCallback(callback_id); shared_response->NewSolution(solution.solution(), solution.solution_info()); EXPECT_EQ(num_solutions, 2); } } TEST(SharedClausesManagerTest, SyncApi) { SharedClausesManager manager(/*always_synchronize=*/true); EXPECT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); EXPECT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); manager.AddBinaryClause(/*id=*/0, 1, 2); std::vector> new_clauses; manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_EQ(1, new_clauses.size()); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(1, 2))); manager.AddBinaryClause(/*id=*/1, 2, 3); manager.AddBinaryClause(/*id=*/1, 3, 2); manager.AddBinaryClause(/*id=*/0, 0, 1); manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(2, 3), std::make_pair(0, 1))); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(0, 1))); manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_TRUE(new_clauses.empty()); } TEST(UniqueClauseStreamTest, AddIgnoresDuplicates) { UniqueClauseStream stream; EXPECT_TRUE(stream.Add({1, 2, 3})); EXPECT_FALSE(stream.Add({3, 2, 1})); EXPECT_EQ(stream.NumBufferedLiterals(), 3); } TEST(UniqueClauseStreamTest, AddIgnoresInvalidSizeClauses) { UniqueClauseStream stream; std::vector long_clause; long_clause.resize(UniqueClauseStream::kMaxClauseSize + 1); for (int i = 0; i < long_clause.size(); ++i) long_clause[i] = i; EXPECT_FALSE(stream.Add({2, 1})); EXPECT_FALSE(stream.Add(long_clause)); EXPECT_EQ(stream.NumBufferedLiterals(), 0); } TEST(UniqueClauseStreamTest, ExportsShortestClauses) { UniqueClauseStream stream; for (int i = 0; i < 1024 / 4; ++i) { stream.Add({i + 1, i + 256, i + 512, -4}); } for (int i = 0; i < 1024 / 3; ++i) { stream.Add({i + 1, i + 256, i + 512}); } for (int i = 0; i < 1024 / 5; ++i) { stream.Add({i + 1, i + 256, i + 512, i + 1024, -2048}); } // Batch 1 should be filled with size 3 clauses. EXPECT_EQ(stream.NextBatch().size(), UniqueClauseStream::kMaxLiteralsPerBatch / 3); // Batch 2 should be empty. EXPECT_TRUE(stream.NextBatch().empty()); } TEST(UniqueClauseStreamTest, DropsClauses) { UniqueClauseStream stream; int literals_successfully_added = 0; for (int i = 0; i < 256 / 4; ++i) { literals_successfully_added += 4 * stream.Add({i + 1, i + 256, i + 512, -4}); } for (int i = 0; i < UniqueClauseStream::kMaxLiteralsPerBatch / 3; ++i) { literals_successfully_added += 3 * stream.Add({i + 1, i + 256, i + 512}); } for (int i = 0; i < 1024 * 1024 / 5; ++i) { literals_successfully_added += 5 * stream.Add({i + 1, i + 256, i + 512, i + 1024, -2048}); } EXPECT_GT(stream.NumBufferedLiterals(), UniqueClauseStream::kMaxLiteralsPerBatch - 5); // Batch should be filled with size 3 clauses. EXPECT_EQ(stream.NextBatch().size(), UniqueClauseStream::kMaxLiteralsPerBatch / 3); EXPECT_TRUE(stream.NextBatch().empty()); } TEST(SharedClausesManagerTest, NonSyncApi) { SharedClausesManager manager(/*always_synchronize=*/false); EXPECT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); EXPECT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); manager.AddBinaryClause(/*id=*/0, 1, 2); std::vector> new_clauses; manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.Synchronize(); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_EQ(1, new_clauses.size()); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(1, 2))); manager.AddBinaryClause(/*id=*/1, 2, 3); manager.AddBinaryClause(/*id=*/1, 3, 2); manager.AddBinaryClause(/*id=*/0, 0, 1); // Not synced. manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_TRUE(new_clauses.empty()); // After sync. manager.Synchronize(); manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(2, 3), std::make_pair(0, 1))); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_THAT(new_clauses, ::testing::ElementsAre(std::make_pair(0, 1))); // Not synced. manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_TRUE(new_clauses.empty()); // After sync. manager.Synchronize(); manager.GetUnseenBinaryClauses(/*id=*/0, &new_clauses); EXPECT_TRUE(new_clauses.empty()); manager.GetUnseenBinaryClauses(/*id=*/1, &new_clauses); EXPECT_TRUE(new_clauses.empty()); } TEST(SharedClausesManagerTest, ShareGlueClauses) { SharedClausesManager manager(/*always_synchronize=*/true); ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; // Add a bunch of clauses that will be skipped batch. for (int i = 0; i < UniqueClauseStream::kMaxLiteralsPerBatch / 8; ++i) { EXPECT_TRUE(stream0.Add({1, 2, 3, 4, 5, 6, 7, i + 8})); } EXPECT_EQ(stream0.NumBufferedLiterals(), UniqueClauseStream::kMaxLiteralsPerBatch); // Fill 1 batch of shorter clauses. for (int i = 0; i < UniqueClauseStream::kMaxLiteralsPerBatch / 4; ++i) { stream1.Add({1, 2, 3, i + 4}); } manager.AddBatch(0, stream0.NextBatch()); manager.AddBatch(1, stream1.NextBatch()); manager.Synchronize(); EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(UniqueClauseStream::kMaxLiteralsPerBatch / 4)); EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(UniqueClauseStream::kMaxLiteralsPerBatch / 4)); EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); } TEST(SharedClausesManagerTest, LbdThresholdIncrease) { SharedClausesManager manager(/*always_synchronize=*/true); ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; const int kExpectedClauses = UniqueClauseStream::kMaxLiteralsPerBatch / 5; for (int i = 0; i < kExpectedClauses; ++i) { stream0.Add({i + 1, i + 513, 2048, 2049, -10}); stream1.Add({i + 1, i + 513, 2048, 2049, -10}); } manager.AddBatch(0, stream0.NextBatch()); manager.AddBatch(1, stream1.NextBatch()); manager.Synchronize(); EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(kExpectedClauses)); EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(kExpectedClauses)); EXPECT_EQ(stream0.lbd_threshold(), 2); EXPECT_EQ(stream1.lbd_threshold(), 2); manager.Synchronize(); manager.AddBatch(0, stream0.NextBatch()); manager.AddBatch(1, stream1.NextBatch()); EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::IsEmpty()); EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::IsEmpty()); EXPECT_EQ(stream0.lbd_threshold(), 3); EXPECT_EQ(stream1.lbd_threshold(), 3); } TEST(SharedClausesManagerTest, LbdThresholdDecrease) { SharedClausesManager manager(/*always_synchronize=*/true); ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; manager.AddBatch(0, stream0.NextBatch()); manager.AddBatch(1, stream1.NextBatch()); const int kSize4Clauses = UniqueClauseStream::kMaxLiteralsPerBatch / 4 / 2; const int kSize5ClausesAdded = UniqueClauseStream::kMaxLiteralsPerBatch / 5; // Then add 1/2 batch of size 4 clauses to each worker. for (int i = 0; i < kSize4Clauses; ++i) { stream0.Add({i + 1, i + 512, 2048, 2049}); stream1.Add({i + 1, i + 513, 2048, -123}); } // Than add loads of longer clauses to just stream0. for (int i = 0; i < kSize5ClausesAdded; ++i) { stream0.Add({i + 1, 2, 3, -10, 12}); } EXPECT_EQ(stream0.lbd_threshold(), 3); EXPECT_EQ(stream1.lbd_threshold(), 3); manager.AddBatch(0, stream0.NextBatch()); manager.AddBatch(1, stream1.NextBatch()); manager.Synchronize(); EXPECT_THAT(manager.GetUnseenClauses(0), ::testing::SizeIs(2 * kSize4Clauses)); EXPECT_THAT(manager.GetUnseenClauses(1), ::testing::SizeIs(2 * kSize4Clauses)); EXPECT_EQ(stream0.lbd_threshold(), 2); } } // namespace } // namespace sat } // namespace operations_research