395 lines
13 KiB
C++
395 lines
13 KiB
C++
// 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 <algorithm>
|
|
#include <limits>
|
|
#include <random>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/log/log.h"
|
|
#include "absl/strings/str_join.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/cp_model_solver.h"
|
|
#include "ortools/sat/model.h"
|
|
#include "ortools/sat/sat_parameters.pb.h"
|
|
#include "ortools/util/stats.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
namespace {
|
|
|
|
using ::google::protobuf::contrib::parse_proto::ParseTestProto;
|
|
|
|
CpModelProto CreateExactlyOneTrueBooleanCpModel(int size) {
|
|
CpModelProto model_proto;
|
|
auto* exactly_one_constraint =
|
|
model_proto.add_constraints()->mutable_exactly_one();
|
|
DecisionStrategyProto* const search_strategy =
|
|
model_proto.add_search_strategy();
|
|
|
|
for (int i = 0; i < size; ++i) {
|
|
IntegerVariableProto* const var = model_proto.add_variables();
|
|
var->add_domain(0);
|
|
var->add_domain(1);
|
|
exactly_one_constraint->add_literals(i);
|
|
search_strategy->add_variables(i);
|
|
}
|
|
return model_proto;
|
|
}
|
|
|
|
TEST(RandomSearchTest, CheckDistribution) {
|
|
const int kSize = 50;
|
|
std::vector<int> winners(kSize, 0);
|
|
const int kLoops = 100;
|
|
for (int l = 0; l < kLoops; ++l) {
|
|
const CpModelProto model_proto = CreateExactlyOneTrueBooleanCpModel(kSize);
|
|
Model model;
|
|
SatParameters parameters;
|
|
parameters.set_search_random_variable_pool_size(10);
|
|
parameters.set_cp_model_presolve(false);
|
|
parameters.set_search_branching(SatParameters::FIXED_SEARCH);
|
|
parameters.set_random_seed(l);
|
|
parameters.set_num_workers(1);
|
|
model.Add(NewSatParameters(parameters));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
for (int i = 0; i < kSize; ++i) {
|
|
if (response.solution(i)) {
|
|
winners[i]++;
|
|
}
|
|
}
|
|
}
|
|
for (int i = 0; i < kSize; ++i) {
|
|
EXPECT_LE(winners[i], kLoops / 10);
|
|
}
|
|
}
|
|
|
|
TEST(RandomSearchTest, CheckSeed) {
|
|
const int kSeeds = 10;
|
|
for (int seed = 0; seed < kSeeds; ++seed) {
|
|
const int kSize = 20;
|
|
std::vector<int> winners(kSize, 0);
|
|
const int kLoops = 50;
|
|
for (int l = 0; l < kLoops; ++l) {
|
|
const CpModelProto model_proto =
|
|
CreateExactlyOneTrueBooleanCpModel(kSize);
|
|
|
|
SatParameters params;
|
|
params.set_randomize_search(true);
|
|
params.set_cp_model_presolve(false);
|
|
params.set_search_branching(SatParameters::FIXED_SEARCH);
|
|
params.set_use_absl_random(false); // Otherwise, each solve changes.
|
|
params.set_random_seed(0);
|
|
params.set_num_workers(1);
|
|
const CpSolverResponse response =
|
|
SolveWithParameters(model_proto, params);
|
|
for (int i = 0; i < kSize; ++i) {
|
|
if (response.solution(i)) {
|
|
winners[i]++;
|
|
}
|
|
}
|
|
}
|
|
for (int i = 0; i < kSize; ++i) {
|
|
EXPECT_TRUE(winners[i] == 0 || winners[i] == kLoops) << winners[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, Default) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 4, 50 ] }
|
|
variables { domain: [ 3, 7 ] }
|
|
variables { domain: [ 0, 7 ] }
|
|
variables { domain: [ 4, 5 ] }
|
|
variables { domain: [ 3, 9 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
exprs { vars: 2 coeffs: 1 }
|
|
exprs { vars: 3 coeffs: 1 }
|
|
exprs { vars: 4 coeffs: 1 }
|
|
}
|
|
}
|
|
)pb");
|
|
Model model;
|
|
model.Add(NewSatParameters(
|
|
"cp_model_presolve:false,search_branching:FIXED_SEARCH,num_workers:1"));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(4, 3, 0, 5, 6));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, ReverseOrder) {
|
|
// Note that SELECT_LOWER_HALF or SELECT_MIN_VALUE result in the same
|
|
// solution.
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 4, 50 ] }
|
|
variables { domain: [ 3, 7 ] }
|
|
variables { domain: [ 0, 7 ] }
|
|
variables { domain: [ 4, 5 ] }
|
|
variables { domain: [ 3, 9 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
exprs { vars: 2 coeffs: 1 }
|
|
exprs { vars: 3 coeffs: 1 }
|
|
exprs { vars: 4 coeffs: 1 }
|
|
}
|
|
}
|
|
search_strategy {
|
|
variables: [ 4, 3, 2, 1, 0 ]
|
|
variable_selection_strategy: CHOOSE_FIRST
|
|
domain_reduction_strategy: SELECT_LOWER_HALF
|
|
}
|
|
)pb");
|
|
Model model;
|
|
model.Add(NewSatParameters(
|
|
"cp_model_presolve:false,search_branching:FIXED_SEARCH,num_workers:1"));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(6, 5, 0, 4, 3));
|
|
}
|
|
|
|
// The strategies that sort variables according to their domain do not have
|
|
// a fixed solution depending on the propagation strength...
|
|
TEST(BasicFixedSearchBehaviorTest, MinDomainSize) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 4, 10 ] }
|
|
variables { domain: [ 3, 7 ] }
|
|
variables { domain: [ 0, 7 ] }
|
|
variables { domain: [ 4, 5 ] }
|
|
variables { domain: [ 3, 9 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
exprs { vars: 2 coeffs: 1 }
|
|
exprs { vars: 3 coeffs: 1 }
|
|
exprs { vars: 4 coeffs: 1 }
|
|
}
|
|
}
|
|
search_strategy {
|
|
variables: [ 0, 1, 2, 3, 4 ]
|
|
variable_selection_strategy: CHOOSE_MIN_DOMAIN_SIZE
|
|
domain_reduction_strategy: SELECT_MAX_VALUE
|
|
}
|
|
)pb");
|
|
Model model;
|
|
model.Add(NewSatParameters(
|
|
"cp_model_presolve:false,search_branching:FIXED_SEARCH,num_workers:1"));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(10, 7, 6, 5, 9));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, WithTransformation1) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 3, 10 ] }
|
|
variables { domain: [ 3, 7 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
}
|
|
}
|
|
search_strategy {
|
|
exprs { vars: 0 coeffs: 1 offset: 4 }
|
|
exprs { vars: 1 coeffs: 4 }
|
|
variable_selection_strategy: CHOOSE_LOWEST_MIN
|
|
domain_reduction_strategy: SELECT_MIN_VALUE
|
|
}
|
|
)pb");
|
|
Model model;
|
|
model.Add(NewSatParameters(
|
|
"cp_model_presolve:false,search_branching:FIXED_SEARCH,num_workers:1"));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(3, 4));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, WithTransformation2) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 3, 7 ] }
|
|
variables { domain: [ 3, 7 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
}
|
|
}
|
|
search_strategy {
|
|
exprs { vars: 0 coeffs: -1 offset: 4 }
|
|
exprs { vars: 1 coeffs: -4 }
|
|
variable_selection_strategy: CHOOSE_LOWEST_MIN
|
|
domain_reduction_strategy: SELECT_MIN_VALUE
|
|
}
|
|
)pb");
|
|
Model model;
|
|
model.Add(NewSatParameters(
|
|
"cp_model_presolve:false,search_branching:FIXED_SEARCH,num_workers:1"));
|
|
const CpSolverResponse response = SolveCpModel(model_proto, &model);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(6, 7));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, MedianTest) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 0, 8 ] }
|
|
variables { domain: [ 0, 8 ] }
|
|
constraints {
|
|
linear {
|
|
vars: [ 0, 1 ]
|
|
coeffs: [ 1, 1 ]
|
|
domain: [ 8, 100 ]
|
|
}
|
|
}
|
|
search_strategy {
|
|
variables: [ 0, 1 ]
|
|
variable_selection_strategy: CHOOSE_FIRST
|
|
domain_reduction_strategy: SELECT_MEDIAN_VALUE
|
|
}
|
|
)pb");
|
|
SatParameters params;
|
|
params.set_keep_all_feasible_solutions_in_presolve(true);
|
|
params.set_search_branching(SatParameters::FIXED_SEARCH);
|
|
params.set_num_workers(1);
|
|
const CpSolverResponse response = SolveWithParameters(model_proto, params);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(4, 6));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, MedianTest2) {
|
|
const CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 0, 20 ] }
|
|
variables { domain: [ 6, 12 ] }
|
|
constraints {
|
|
all_diff {
|
|
exprs { vars: 0 coeffs: 1 }
|
|
exprs { vars: 1 coeffs: 1 }
|
|
}
|
|
}
|
|
search_strategy {
|
|
variables: [ 0, 1 ]
|
|
variable_selection_strategy: CHOOSE_MAX_DOMAIN_SIZE
|
|
domain_reduction_strategy: SELECT_MEDIAN_VALUE
|
|
}
|
|
)pb");
|
|
SatParameters params;
|
|
params.set_keep_all_feasible_solutions_in_presolve(true);
|
|
params.set_search_branching(SatParameters::FIXED_SEARCH);
|
|
params.set_num_workers(1);
|
|
const CpSolverResponse response = SolveWithParameters(model_proto, params);
|
|
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
EXPECT_THAT(response.solution(), testing::ElementsAre(10, 8));
|
|
}
|
|
|
|
TEST(BasicFixedSearchBehaviorTest, RandomHalfTest) {
|
|
CpModelProto model_proto = ParseTestProto(R"pb(
|
|
variables { domain: [ 0, 1 ] }
|
|
variables { domain: [ 0, 10 ] }
|
|
variables { domain: [ 0, 10 ] }
|
|
variables { domain: [ 0, 10 ] }
|
|
constraints {
|
|
linear {
|
|
vars: [ 0, 1, 2, 3 ]
|
|
coeffs: [ 1, 1, 1, 1 ]
|
|
domain: [ 10, 10 ]
|
|
}
|
|
}
|
|
search_strategy {
|
|
variables: [ 0, 1, 2, 3 ]
|
|
variable_selection_strategy: CHOOSE_FIRST
|
|
domain_reduction_strategy: SELECT_RANDOM_HALF
|
|
}
|
|
)pb");
|
|
|
|
absl::flat_hash_map<std::tuple<int, int, int, int>, int> count_by_solution;
|
|
{
|
|
SatParameters params;
|
|
params.set_enumerate_all_solutions(true);
|
|
params.set_num_workers(1);
|
|
Model model;
|
|
model.Add(NewSatParameters(params));
|
|
model.Add(NewFeasibleSolutionObserver(
|
|
[&count_by_solution](const CpSolverResponse& response) {
|
|
count_by_solution[std::make_tuple(
|
|
response.solution(0), response.solution(1), response.solution(2),
|
|
response.solution(3))] = 0;
|
|
}));
|
|
EXPECT_EQ(SolveCpModel(model_proto, &model).status(),
|
|
CpSolverStatus::OPTIMAL);
|
|
}
|
|
constexpr int kNumExpectedSolutions = 121;
|
|
EXPECT_EQ(count_by_solution.size(), kNumExpectedSolutions);
|
|
|
|
// Repeatedly solve the model with a different seed and count the number of
|
|
// times each solution occurs. If each solution is found with equal
|
|
// probability, each solution should be found "near" kExpectedMean times.
|
|
constexpr int kExpectedMean = 100;
|
|
std::mt19937_64 random(12345);
|
|
for (int i = 0; i < kNumExpectedSolutions * kExpectedMean; ++i) {
|
|
SatParameters params;
|
|
params.set_cp_model_presolve(false);
|
|
params.set_search_branching(SatParameters::FIXED_SEARCH);
|
|
params.set_random_seed(i);
|
|
params.set_num_workers(1);
|
|
auto search_order =
|
|
model_proto.mutable_search_strategy(0)->mutable_variables();
|
|
std::shuffle(search_order->begin(), search_order->end(), random);
|
|
const CpSolverResponse response = SolveWithParameters(model_proto, params);
|
|
EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL);
|
|
count_by_solution[std::make_tuple(
|
|
response.solution(0), response.solution(1), response.solution(2),
|
|
response.solution(3))]++;
|
|
}
|
|
EXPECT_EQ(count_by_solution.size(), kNumExpectedSolutions);
|
|
DoubleDistribution counts;
|
|
int min_count = std::numeric_limits<int>::max();
|
|
std::tuple<int, int, int, int> min_count_solution;
|
|
int max_count = 0;
|
|
std::tuple<int, int, int, int> max_count_solution;
|
|
for (const auto& [solution, count] : count_by_solution) {
|
|
if (count < min_count) {
|
|
min_count = count;
|
|
min_count_solution = solution;
|
|
}
|
|
if (count > max_count) {
|
|
max_count = count;
|
|
max_count_solution = solution;
|
|
}
|
|
counts.Add(count);
|
|
}
|
|
const double std_dev = counts.StdDeviation();
|
|
LOG(INFO) << "min_count = " << min_count << " for "
|
|
<< absl::StrJoin(min_count_solution, ",");
|
|
LOG(INFO) << "max_count = " << max_count << " for "
|
|
<< absl::StrJoin(max_count_solution, ",");
|
|
LOG(INFO) << "std_dev / mean = " << std_dev / kExpectedMean;
|
|
EXPECT_GE(min_count, kExpectedMean / 10);
|
|
// If each solution was really found with equal probability, the coefficient
|
|
// of variation would be much lower (about 0.1 for kExpectedMean = 100).
|
|
EXPECT_LE(std_dev / kExpectedMean, 0.5);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace sat
|
|
} // namespace operations_research
|