diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index e95cd5c8d6b..a059f9ca4ae 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -53,9 +53,9 @@ cc_binary( srcs = ["binary_search_benchmark.cc"], deps = [ ":binary_search", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/numeric:int128", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -103,6 +103,7 @@ cc_binary( srcs = ["radix_sort_benchmark.cc"], deps = [ ":radix_sort", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log", "@abseil-cpp//absl/numeric:bits", @@ -112,7 +113,6 @@ cc_binary( "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -121,12 +121,12 @@ cc_binary( srcs = ["radix_sort_release_benchmark.cc"], deps = [ ":radix_sort", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -321,6 +321,7 @@ cc_binary( srcs = ["space_saving_most_frequent_benchmark.cc"], deps = [ ":space_saving_most_frequent", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/hash", @@ -328,7 +329,6 @@ cc_binary( "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -523,9 +523,9 @@ cc_binary( deps = [ ":multikey_radix_sort", ":radix_sort", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/random", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index ab8881832cc..3fa5f157a21 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -113,15 +113,25 @@ cc_binary( srcs = ["constant_divisor_benchmark.cc"], deps = [ ":constant_divisor", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) +cc_library( + name = "benchmark_main", + srcs = ["benchmark_main.cc"], + deps = [ + "//ortools/base", + "@google_benchmark//:benchmark", + ], + alwayslink = True, +) + cc_library( name = "container_logging", hdrs = ["container_logging.h"], diff --git a/ortools/base/CMakeLists.txt b/ortools/base/CMakeLists.txt index 861587487c3..2e54eff1651 100644 --- a/ortools/base/CMakeLists.txt +++ b/ortools/base/CMakeLists.txt @@ -14,6 +14,7 @@ file(GLOB _SRCS "*.h" "*.cc") list(FILTER _SRCS EXCLUDE REGEX ".*/.*_benchmark.cc") list(FILTER _SRCS EXCLUDE REGEX ".*/.*_test.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/benchmark_main.cc") list(FILTER _SRCS EXCLUDE REGEX "/gmock\.h") set(NAME ${PROJECT_NAME}_base) diff --git a/ortools/base/benchmark_main.cc b/ortools/base/benchmark_main.cc new file mode 100644 index 00000000000..13df25c5e22 --- /dev/null +++ b/ortools/base/benchmark_main.cc @@ -0,0 +1,26 @@ +// 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 + +#include "benchmark/benchmark.h" +#include "ortools/base/init_google.h" + +int main(int argc, char* argv[]) { + benchmark::MaybeReenterWithoutASLR(argc, argv); + benchmark::Initialize(&argc, argv); + InitGoogle(argv[0], &argc, &argv, false); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + return EXIT_SUCCESS; +} diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index 9182c029855..581cf9a7d53 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -59,13 +59,13 @@ cc_binary( srcs = ["bounded_dijkstra_benchmark.cc"], deps = [ ":bounded_dijkstra", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "//ortools/graph_base:test_util", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -170,6 +170,7 @@ cc_binary( srcs = ["cliques_benchmark.cc"], deps = [ ":cliques", + "//ortools/base:benchmark_main", "//ortools/util:time_limit", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/functional:bind_front", @@ -178,7 +179,6 @@ cc_binary( "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -251,9 +251,9 @@ cc_binary( srcs = ["christofides_benchmark.cc"], deps = [ ":christofides", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/log:check", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -280,10 +280,10 @@ cc_binary( srcs = ["eulerian_path_benchmark.cc"], deps = [ ":eulerian_path", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/log:check", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -316,11 +316,11 @@ cc_binary( srcs = ["minimum_spanning_tree_benchmark.cc"], deps = [ ":minimum_spanning_tree", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -423,11 +423,11 @@ cc_binary( deps = [ ":k_shortest_paths", ":shortest_paths", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -507,6 +507,7 @@ cc_binary( srcs = ["generic_max_flow_benchmark.cc"], deps = [ ":generic_max_flow", + "//ortools/base:benchmark_main", "//ortools/graph_base:flow_graph", "//ortools/graph_base:graph", "@abseil-cpp//absl/log:check", @@ -514,7 +515,6 @@ cc_binary( "@abseil-cpp//absl/random:bit_gen_ref", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -570,12 +570,12 @@ cc_binary( srcs = ["min_cost_flow_benchmark.cc"], deps = [ ":min_cost_flow", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -635,10 +635,10 @@ cc_binary( srcs = ["assignment_benchmark.cc"], deps = [ ":assignment", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -678,11 +678,11 @@ cc_binary( srcs = ["linear_assignment_benchmark.cc"], deps = [ ":linear_assignment", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -792,6 +792,7 @@ cc_binary( srcs = ["dag_constrained_shortest_path_benchmark.cc"], deps = [ ":dag_constrained_shortest_path", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log:check", @@ -799,7 +800,6 @@ cc_binary( "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -836,11 +836,11 @@ cc_binary( srcs = ["rooted_tree_benchmark.cc"], deps = [ ":rooted_tree", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -870,9 +870,9 @@ cc_binary( srcs = ["minimum_vertex_cover_benchmark.cc"], deps = [ ":minimum_vertex_cover", + "//ortools/base:benchmark_main", "@abseil-cpp//absl/algorithm:container", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) @@ -919,12 +919,12 @@ cc_binary( srcs = ["dag_shortest_path_benchmark.cc"], deps = [ ":dag_shortest_path", + "//ortools/base:benchmark_main", "//ortools/graph_base:graph", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", "@abseil-cpp//absl/types:span", "@google_benchmark//:benchmark", - "@google_benchmark//:benchmark_main", ], ) diff --git a/ortools/set_cover/BUILD.bazel b/ortools/set_cover/BUILD.bazel index b7804f61fe2..9972805969c 100644 --- a/ortools/set_cover/BUILD.bazel +++ b/ortools/set_cover/BUILD.bazel @@ -63,6 +63,16 @@ cc_test( "//ortools/base:gmock_main", "@abseil-cpp//absl/random", "@abseil-cpp//absl/types:span", + ], +) + +cc_binary( + name = "base_types_benchmark", + srcs = ["base_types_benchmark.cc"], + deps = [ + ":base_types", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/random", "@google_benchmark//:benchmark", ], ) @@ -338,6 +348,42 @@ cc_test( ], ) +cc_test( + name = "knights_test", + size = "medium", + timeout = "eternal", + srcs = ["knights_test.cc"], + tags = ["manual"], + deps = [ + ":base_types", + ":set_cover_cc_proto", + ":set_cover_heuristics", + ":set_cover_invariant", + ":set_cover_mip", + ":set_cover_model", + "//ortools/base:gmock_main", + "//ortools/math_opt/solvers:glop_solver", + "//ortools/math_opt/solvers:gscip_solver", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/time", + ], +) + +cc_binary( + name = "knights_benchmark", + srcs = ["knights_benchmark.cc"], + deps = [ + ":base_types", + ":set_cover_heuristics", + ":set_cover_invariant", + ":set_cover_model", + "//ortools/base:benchmark_main", + "@google_benchmark//:benchmark", + ], +) + # Side constraint: capacity. proto_library( diff --git a/ortools/set_cover/base_types_benchmark.cc b/ortools/set_cover/base_types_benchmark.cc new file mode 100644 index 00000000000..efef6c7f576 --- /dev/null +++ b/ortools/set_cover/base_types_benchmark.cc @@ -0,0 +1,55 @@ +// 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 +#include +#include +#include + +#include "absl/random/random.h" +#include "benchmark/benchmark.h" +#include "ortools/set_cover/base_types.h" + +namespace operations_research { +namespace { + +SparseRow GenerateRandomSparseRow(size_t size, int64_t max_value) { + SparseRow sparse_row; + sparse_row.reserve(size); + absl::BitGen gen; + std::uniform_int_distribution dist(0, max_value); + SubsetIndex current_value(0); + for (size_t i = 0; i < size; ++i) { + current_value += SubsetIndex(dist(gen)); + sparse_row.push_back(current_value); + } + return sparse_row; +} + +static void BM_StrongVectorIteration(benchmark::State& state) { + const size_t size = state.range(0); + const int64_t delta_range = state.range(1); + SparseRow strong_vector = GenerateRandomSparseRow(size, delta_range); + for (auto _ : state) { + int64_t sum = 0; + for (const auto& x : strong_vector) { + sum += x.value(); + } + benchmark::DoNotOptimize(sum); // Prevent optimization + } +} +BENCHMARK(BM_StrongVectorIteration) + ->ArgsProduct({{100'000, 100'000'000}, {1 << 8, 1 << 16}}); + +} // namespace +} // namespace operations_research diff --git a/ortools/set_cover/knights_benchmark.cc b/ortools/set_cover/knights_benchmark.cc new file mode 100644 index 00000000000..950c09d0fbf --- /dev/null +++ b/ortools/set_cover/knights_benchmark.cc @@ -0,0 +1,75 @@ +// 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 +#include + +#include "benchmark/benchmark.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +namespace { + +class KnightsCover { + public: + KnightsCover(int num_rows, int num_cols) + : num_rows_(num_rows), num_cols_(num_cols), model_() { + constexpr int knight_row_move[] = {2, 1, -1, -2, -2, -1, 1, 2}; + constexpr int knight_col_move[] = {1, 2, 2, 1, -1, -2, -2, -1}; + for (int row = 0; row < num_rows_; ++row) { + for (int col = 0; col < num_cols_; ++col) { + model_.AddEmptySubset(1); + model_.AddElementToLastSubset(ElementNumber(row, col)); + for (int i = 0; i < 8; ++i) { + const int new_row = row + knight_row_move[i]; + const int new_col = col + knight_col_move[i]; + if (IsOnBoard(new_row, new_col)) { + model_.AddElementToLastSubset(ElementNumber(new_row, new_col)); + } + } + } + } + } + + SetCoverModel model() const { return model_; } + + private: + bool IsOnBoard(int row, int col) const { + return row >= 0 && row < num_rows_ && col >= 0 && col < num_cols_; + } + ElementIndex ElementNumber(int row, int col) const { + return ElementIndex(row * num_cols_ + col); + } + int num_rows_; + int num_cols_; + SetCoverModel model_; +}; + +static constexpr int SIZE = 128; + +void BM_Steepest(benchmark::State& state) { + for (auto s : state) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + SetCoverInvariant inv(&model); + GreedySolutionGenerator greedy(&inv); + SteepestSearch steepest(&inv); + } +} + +BENCHMARK(BM_Steepest)->Arg(1 << 5); + +} // namespace +} // namespace operations_research diff --git a/ortools/set_cover/knights_test.cc b/ortools/set_cover/knights_test.cc new file mode 100644 index 00000000000..bd55baa7e2f --- /dev/null +++ b/ortools/set_cover/knights_test.cc @@ -0,0 +1,513 @@ +// 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 +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/strings/str_cat.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover.pb.h" +#include "ortools/set_cover/set_cover_heuristics.h" +#include "ortools/set_cover/set_cover_invariant.h" +#include "ortools/set_cover/set_cover_mip.h" +#include "ortools/set_cover/set_cover_model.h" + +namespace operations_research { +namespace { +using CL = SetCoverInvariant::ConsistencyLevel; + +class KnightsCover { + public: + KnightsCover(int num_rows, int num_cols) + : num_rows_(num_rows), num_cols_(num_cols), model_() { + constexpr int knight_row_move[] = {2, 1, -1, -2, -2, -1, 1, 2}; + constexpr int knight_col_move[] = {1, 2, 2, 1, -1, -2, -2, -1}; + for (int row = 0; row < num_rows_; ++row) { + for (int col = 0; col < num_cols_; ++col) { + model_.AddEmptySubset(1); + model_.AddElementToLastSubset(ElementNumber(row, col)); + for (int i = 0; i < 8; ++i) { + const int new_row = row + knight_row_move[i]; + const int new_col = col + knight_col_move[i]; + if (IsOnBoard(new_row, new_col)) { + model_.AddElementToLastSubset(ElementNumber(new_row, new_col)); + } + } + } + } + } + + SetCoverModel model() const { return model_; } + + void DisplaySolutionAscii(const SubsetBoolVector& choices) const { + std::string line; + std::string separator = "+"; + for (int col = 0; col < num_cols_; ++col) { + absl::StrAppend(&separator, "-+"); + } + LOG(INFO) << separator; + for (int row = 0; row < num_rows_; ++row) { + line = "|"; + for (int col = 0; col < num_cols_; ++col) { + const SubsetIndex subset(SubsetNumber(row, col)); + absl::StrAppend(&line, choices[subset] ? "X|" : " |"); + } + LOG(INFO) << line; + LOG(INFO) << separator; + } + } + + void DisplaySolutionUnicode(const SubsetBoolVector& choices) const { + for (int row = 0; row < num_rows_; ++row) { + std::string line; + for (int col = 0; col < num_cols_; ++col) { + const SubsetIndex subset(SubsetNumber(row, col)); + + absl::StrAppend(&line, choices[subset] ? "♞" : "."); + } + LOG(INFO) << line; + } + } + + void DisplaySolution(const SubsetBoolVector& choices) const { + DisplaySolutionUnicode(choices); + } + + std::vector ClearSubsetWithinRadius( + const SetCoverInvariant::ConsistencyLevel consistency, int row, int col, + int radius, SetCoverInvariant* inv) const { + std::vector cleared_subsets; + cleared_subsets.reserve((2 * radius + 1) * (2 * radius + 1)); + for (int r = row - radius; r <= row + radius; ++r) { + for (int c = col - radius; c <= col + radius; ++c) { + if (!IsOnBoard(r, c)) continue; + const SubsetIndex subset(SubsetNumber(r, c)); + if (inv->is_selected()[subset]) { + inv->Deselect(subset, consistency); + cleared_subsets.push_back(subset); + } + } + } + return cleared_subsets; + } + + private: + bool IsOnBoard(int row, int col) const { + return row >= 0 && row < num_rows_ && col >= 0 && col < num_cols_; + } + // There is a 1:1 mapping between elements and subsets because the + // subset i corresponds to placing the element i at the position (r, c). + ElementIndex ElementNumber(int row, int col) const { + return ElementIndex(row * num_cols_ + col); + } + SubsetIndex SubsetNumber(int row, int col) const { + return SubsetIndex(row * num_cols_ + col); + } + int num_rows_; + int num_cols_; + SetCoverModel model_; +}; + +TEST(SetCoverProtoTest, SaveReload) { + SetCoverModel model = KnightsCover(10, 10).model(); + model.SortElementsInSubsets(); + SetCoverProto proto = model.ExportModelAsProto(); + SetCoverModel reloaded; + reloaded.ImportModelFromProto(proto); + EXPECT_EQ(model.num_subsets(), reloaded.num_subsets()); + EXPECT_EQ(model.num_elements(), reloaded.num_elements()); + EXPECT_EQ(model.subset_costs(), reloaded.subset_costs()); + EXPECT_EQ(model.columns(), reloaded.columns()); +} + +TEST(SolutionProtoTest, SaveReloadTwice) { + SetCoverModel model = KnightsCover(3, 3).model(); + SetCoverInvariant inv(&model); + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); + SetCoverSolutionResponse greedy_proto = inv.ExportSolutionAsProto(); + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(500).NextSolution()); + EXPECT_TRUE(inv.CheckConsistency(CL::kRedundancy)); + SetCoverSolutionResponse steepest_proto = inv.ExportSolutionAsProto(); + inv.ImportSolutionFromProto(greedy_proto); + CHECK(steepest.SetMaxIterations(500).NextSolution()); + EXPECT_TRUE(inv.CheckConsistency(CL::kRedundancy)); +} + +#ifdef NDEBUG +static constexpr int SIZE = 128; +#else +static constexpr int SIZE = 16; +#endif + +TEST(SetCoverTest, KnightsCoverCreation) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + EXPECT_TRUE(model.ComputeFeasibility()); +} + +TEST(SetCoverTest, KnightsCoverTrivalAndGreedy) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + EXPECT_TRUE(model.ComputeFeasibility()); + SetCoverInvariant inv(&model); + + TrivialSolutionGenerator trivial(&inv); + CHECK(trivial.NextSolution()); + LOG(INFO) << "TrivialSolutionGenerator cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kCostAndCoverage)); + + // Reinitialize before using Greedy, to start from scratch. + inv.Initialize(); + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + LOG(INFO) << "GreedySolutionGenerator cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100'000).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); +} + +TEST(SetCoverTest, KnightsCoverGreedy) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + SetCoverInvariant inv(&model); + + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + LOG(INFO) << "GreedySolutionGenerator cost: " << inv.cost(); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); +} + +TEST(SetCoverTest, KnightsCoverDegree) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + SetCoverInvariant inv(&model); + + ElementDegreeSolutionGenerator degree(&inv); + CHECK(degree.NextSolution()); + LOG(INFO) << "ElementDegreeSolutionGenerator cost: " << inv.cost(); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); +} + +TEST(SetCoverTest, KnightsCoverGLS) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + SetCoverInvariant inv(&model); + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + LOG(INFO) << "GreedySolutionGenerator cost: " << inv.cost(); + GuidedLocalSearch gls(&inv); + CHECK(gls.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "GuidedLocalSearch cost: " << inv.cost(); +} + +TEST(SetCoverTest, KnightsCoverRandom) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + EXPECT_TRUE(model.ComputeFeasibility()); + SetCoverInvariant inv(&model); + + RandomSolutionGenerator random(&inv); + CHECK(random.NextSolution()); + LOG(INFO) << "RandomSolutionGenerator cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kCostAndCoverage)); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); +} + +TEST(SetCoverTest, KnightsCoverTrivial) { + SetCoverModel model = KnightsCover(SIZE, SIZE).model(); + EXPECT_TRUE(model.ComputeFeasibility()); + SetCoverInvariant inv(&model); + + TrivialSolutionGenerator trivial(&inv); + CHECK(trivial.NextSolution()); + LOG(INFO) << "TrivialSolutionGenerator cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kCostAndCoverage)); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); +} + +TEST(SetCoverTest, KnightsCoverGreedyAndTabu) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + LOG(INFO) << "GreedySolutionGenerator cost: " << inv.cost(); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); + + GuidedTabuSearch gts(&inv); + CHECK(gts.SetMaxIterations(1'000).NextSolution()); + LOG(INFO) << "GuidedTabuSearch cost: " << inv.cost(); + EXPECT_TRUE(inv.CheckConsistency(CL::kFreeAndUncovered)); + knights.DisplaySolution(inv.is_selected()); +} + +TEST(SetCoverTest, KnightsCoverGreedyRandomClear) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + Cost best_cost = std::numeric_limits::max(); + SubsetBoolVector best_choices = inv.is_selected(); + for (int i = 0; i < 100; ++i) { + inv.LoadSolution(best_choices); + ClearRandomSubsets(0.1 * inv.trace().size(), &inv); + + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(10'000).NextSolution()); + + if (inv.cost() < best_cost) { + best_cost = inv.cost(); + best_choices = inv.is_selected(); + LOG(INFO) << "Best cost: " << best_cost << " at iteration = " << i; + } + } + inv.LoadSolution(best_choices); + knights.DisplaySolution(best_choices); + LOG(INFO) << "RandomClear cost: " << best_cost; + // The best solution found until 2023-08 has a cost of 350. + // http://www.contestcen.com/kn50.htm + if (BoardSize == 50) { + EXPECT_GE(inv.cost(), 350); + } +} + +TEST(SetCoverTest, KnightsCoverCliqueGuidedLNS) { + // Best known values for the Knight Covering Problem. + // 0-2: trivial/undefined (0). + // 3-10: https://www.contestcen.com/kn3.htm + // 11+: https://www.contestcen.com/knight.htm + const std::vector best_known_cost = { + 0, 0, 0, // 0-2 + 4, 4, 5, 8, 10, 12, 14, 16, // 3-10 + 21, 24, 28, 32, 36, 40, 46, 52, 57, 62, // 11-20 + 68, 75, 82, 88, 95, // 21-25 + // 102, 109, 116, 124, 131, // 26-30 + // 140, 148, 157, 166, 176, 186, 196, 206, 217, 228, // 31-40 + // 239, 250, 261, 273, 285, 297, 309, 322, 335, 350 // 41-50 + }; + + for (int BoardSize = 3; BoardSize < 21; ++BoardSize) { + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + Cost lower_bound = ComputeDualAscentLB(inv, 1000); + LOG(INFO) << "Dual ascent Lower bound: " << lower_bound; + Cost lower_bound_degree = ComputeDegreeBasedDualAscentLB(inv, 1000); + LOG(INFO) << "Degree based dual ascent Lower bound: " << lower_bound_degree; + LOG(INFO) << "Lower bound: " << std::max(lower_bound, lower_bound_degree); + LazyElementDegreeSolutionGenerator degree(&inv); + degree.SetNumRandomPasses(1000); + CHECK(degree.NextSolution()); + LOG(INFO) << "LazyElementDegreeSolutionGenerator cost: " << inv.cost(); + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "LazyElementDegreeSolutionGenerator + SteepestSearch cost: " + << inv.cost(); + CliqueGuidedLNS clique_guided_lns(&inv); + clique_guided_lns.SetMaxCliqueSize(1000).SetMaxNumCliques(400); + clique_guided_lns.SetTimeLimit(absl::Milliseconds(500)); + CHECK(clique_guided_lns.NextSolution()); + LOG(INFO) << "CliqueGuidedLNS cost (" << BoardSize << " * " << BoardSize + << "): " << inv.cost() << " / " << best_known_cost[BoardSize] + << " Time:" << ToInt64Milliseconds(clique_guided_lns.run_time()); + EXPECT_GE(inv.cost(), best_known_cost[BoardSize]); + knights.DisplaySolution(inv.is_selected()); + } +} + +TEST(SetCoverTest, KnightsCoverElementDegreeRandomClear) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + Cost best_cost = std::numeric_limits::max(); + + LazyElementDegreeSolutionGenerator degree(&inv); + LazySteepestSearch steepest(&inv); + std::vector best_trace; + ElementToIntVector best_coverage; + for (int iteration = 0; iteration < 10000; ++iteration) { + CHECK(degree.NextSolution()); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + + if (inv.cost() < best_cost) { + best_cost = inv.cost(); + inv.CompressTrace(); + best_trace = inv.trace(); + best_coverage = inv.coverage(); + LOG(INFO) << "Best cost: " << best_cost + << " at iteration = " << iteration; + } else { + inv.LoadTraceAndCoverage(best_trace, best_coverage); + } + ClearRandomSubsets(0.1 * inv.trace().size(), &inv); + } + inv.LoadTraceAndCoverage(best_trace, best_coverage); + knights.DisplaySolution(inv.is_selected()); + LOG(INFO) << "RandomClear cost: " << best_cost; + // The best solution found until 2023-08 has a cost of 350. + // http://www.contestcen.com/kn50.htm + if (BoardSize == 50) { + EXPECT_GE(inv.cost(), 350); + } +} + +TEST(SetCoverTest, KnightsCoverElementDegreeRadiusClear) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + Cost best_cost = std::numeric_limits::max(); + std::vector best_trace; + ElementToIntVector best_coverage; + int iteration = 0; + LazyElementDegreeSolutionGenerator degree(&inv); + LazySteepestSearch steepest(&inv); + for (int radius = 8; radius >= 1; --radius) { + for (int row = 0; row < BoardSize; ++row) { + for (int col = 0; col < BoardSize; ++col) { + CHECK(degree.NextSolution()); + DCHECK(inv.CheckConsistency(CL::kCostAndCoverage)); + + LazySteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + + if (inv.cost() < best_cost) { + best_cost = inv.cost(); + inv.CompressTrace(); + best_trace = inv.trace(); + best_coverage = inv.coverage(); + LOG(INFO) << "Best cost: " << best_cost + << " at iteration = " << iteration; + } else { + inv.LoadTraceAndCoverage(best_trace, best_coverage); + } + knights.ClearSubsetWithinRadius(CL::kCostAndCoverage, row, col, radius, + &inv); + ++iteration; + } + } + } + inv.LoadTraceAndCoverage(best_trace, best_coverage); + knights.DisplaySolution(inv.is_selected()); + LOG(INFO) << "RadiusClear cost: " << best_cost; + // The best solution found until 2023-08 has a cost of 350. + // http://www.contestcen.com/kn50.htm + if (BoardSize == 50) { + EXPECT_GE(inv.cost(), 350); + } +} + +TEST(SetCoverTest, DISABLED_KnightsCoverRandomClearMip) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + GreedySolutionGenerator greedy(&inv); + CHECK(greedy.NextSolution()); + LOG(INFO) << "GreedySolutionGenerator cost: " << inv.cost(); + + SteepestSearch steepest(&inv); + CHECK(steepest.SetMaxIterations(100).NextSolution()); + LOG(INFO) << "SteepestSearch cost: " << inv.cost(); + + Cost best_cost = inv.cost(); + SubsetBoolVector best_choices = inv.is_selected(); + for (int i = 0; i < 1'000; ++i) { + auto focus = ClearRandomSubsets(0.1 * inv.trace().size(), &inv); + SetCoverMip mip(&inv); + mip.UseIntegers(true).SetTimeLimit(absl::Seconds(1)); + mip.NextSolution(focus); + EXPECT_TRUE(inv.CheckConsistency(CL::kCostAndCoverage)); + if (inv.cost() < best_cost) { + best_cost = inv.cost(); + best_choices = inv.is_selected(); + LOG(INFO) << "Best cost: " << best_cost << " at iteration = " << i; + } + inv.LoadSolution(best_choices); + } + knights.DisplaySolution(best_choices); + LOG(INFO) << "RandomClearMip cost: " << best_cost; +} + +TEST(SetCoverTest, KnightsCoverMip) { +#ifdef NDEBUG + constexpr int BoardSize = 50; +#else + constexpr int BoardSize = 15; +#endif + KnightsCover knights(BoardSize, BoardSize); + SetCoverModel model = knights.model(); + SetCoverInvariant inv(&model); + SetCoverMip mip(&inv); + mip.UseIntegers(true).SetTimeLimit(absl::Milliseconds(500)); + mip.NextSolution(); + LOG(INFO) << "Mip cost: " << inv.cost(); + knights.DisplaySolution(inv.is_selected()); + if (BoardSize == 50) { + EXPECT_GE(inv.cost(), 350); + } +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index 9ea7c4e4d8f..4593f877b9d 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -12,9 +12,12 @@ # limitations under the License. load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@protobuf//bazel:java_proto_library.bzl", "java_proto_library") load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") package(default_visibility = ["//visibility:public"]) @@ -222,10 +225,9 @@ cc_library( # You must also set this flag if you depend on this target and use its methods related to # IEEE-754 rounding modes. copts = select({ - "@platforms//os:linux": ["-frounding-math"], - "@platforms//os:macos": ["-frounding-math"], - "@platforms//os:windows": [], - "//conditions:default": ["-frounding-math"], + "@rules_cc//cc/compiler:clang": ["-frounding-math"], + "@rules_cc//cc/compiler:gcc": ["-frounding-math"], + "//conditions:default": [], }), deps = [ ":bitset", @@ -360,6 +362,26 @@ cc_library( ], ) +proto_library( + name = "status_proto", + srcs = ["status.proto"], +) + +cc_proto_library( + name = "status_cc_proto", + deps = [":status_proto"], +) + +java_proto_library( + name = "status_java_proto", + deps = [":status_proto"], +) + +py_proto_library( + name = "status_py_pb2", + deps = [":status_proto"], +) + cc_library( name = "random_engine", hdrs = ["random_engine.h"], @@ -494,3 +516,342 @@ cc_library( hdrs = ["scheduling.h"], deps = ["@abseil-cpp//absl/log:check"], ) + +cc_test( + name = "dense_set_test", + srcs = ["dense_set_test.cc"], + deps = [ + ":dense_set", + "//ortools/base:gmock_main", + "//ortools/base:strong_int", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + ], +) + +cc_binary( + name = "dense_set_benchmark", + srcs = ["dense_set_benchmark.cc"], + deps = [ + ":dense_set", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "solve_interrupter_test", + srcs = ["solve_interrupter_test.cc"], + deps = [ + ":solve_interrupter", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "fp_roundtrip_conv_testing_test", + srcs = ["fp_roundtrip_conv_testing_test.cc"], + deps = [ + ":fp_roundtrip_conv", + ":fp_roundtrip_conv_testing", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "flat_matrix_test", + srcs = ["flat_matrix_test.cc"], + deps = [ + ":flat_matrix", + ":random_engine", + "//ortools/base:dump_vars", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log:check", + ], +) + +cc_binary( + name = "flat_matrix_benchmark", + srcs = ["flat_matrix_benchmark.cc"], + deps = [ + ":flat_matrix", + ":random_engine", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log:check", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "status_macros_test", + srcs = ["status_macros_test.cc"], + deps = [ + ":status_macros", + "//ortools/base:gmock_main", + "//ortools/base:status_macros", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_test( + name = "logging_test", + size = "small", + srcs = ["logging_test.cc"], + deps = [ + ":logging", + "//ortools/base:gmock_main", + "//ortools/port:scoped_std_stream_capture", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "lazy_mutable_copy_test", + srcs = ["lazy_mutable_copy_test.cc"], + deps = [ + ":lazy_mutable_copy", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "adaptative_parameter_value_test", + srcs = ["adaptative_parameter_value_test.cc"], + deps = [ + ":adaptative_parameter_value", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/random", + ], +) + +cc_test( + name = "random_engine_test", + srcs = ["random_engine_test.cc"], + deps = [ + ":random_engine", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "rev_test", + srcs = ["rev_test.cc"], + deps = [ + ":rev", + ":strong_integers", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/random:distributions", + ], +) + +cc_test( + name = "range_query_function_test", + srcs = ["range_query_function_test.cc"], + deps = [ + ":range_query_function", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/log:check", + ], +) + +cc_test( + name = "running_stat_test", + size = "small", + srcs = ["running_stat_test.cc"], + deps = [ + ":running_stat", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "fp_utils_test", + size = "small", + srcs = ["fp_utils_test.cc"], + deps = [ + ":fp_utils", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/types:span", + ], +) + +cc_binary( + name = "fp_utils_benchmark", + srcs = ["fp_utils_benchmark.cc"], + deps = [ + ":fp_utils", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/log:check", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "time_limit_test", + size = "small", + srcs = ["time_limit_test.cc"], + flaky = 1, # 2014-05-21: 102 failures over 10000 runs (forge, fastbuild). + deps = [ + ":testing_utils", + ":time_limit", + "//ortools/base:gmock_main", + "//ortools/base:log_severity", + "//ortools/base:threadpool", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log:log_streamer", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", + ], +) + +cc_binary( + name = "time_limit_benchmark", + srcs = ["time_limit_benchmark.cc"], + deps = [ + ":time_limit", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "tuple_set_test", + size = "small", + srcs = ["tuple_set_test.cc"], + deps = [ + ":tuple_set", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "rational_approximation_test", + size = "small", + srcs = ["rational_approximation_test.cc"], + deps = [ + ":fp_utils", + ":rational_approximation", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "saturated_arithmetic_test", + srcs = ["saturated_arithmetic_test.cc"], + deps = [ + ":saturated_arithmetic", + ":strong_integers", + "//ortools/base:gmock_main", + "//ortools/base:types", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/numeric:int128", + "@abseil-cpp//absl/random", + ], +) + +cc_binary( + name = "saturated_arithmetic_benchmark", + srcs = ["saturated_arithmetic_benchmark.cc"], + deps = [ + ":saturated_arithmetic", + "//ortools/base:benchmark_main", + "//ortools/base:types", + "@abseil-cpp//absl/log", + "@google_benchmark//:benchmark", + ], +) + +cc_test( + name = "permutation_test", + size = "small", + srcs = ["permutation_test.cc"], + deps = [ + ":permutation", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "zvector_test", + size = "large", + srcs = ["zvector_test.cc"], + deps = [ + ":zvector", + "//ortools/base:gmock_main", + "@abseil-cpp//absl/random", + ], +) + +cc_test( + name = "cached_log_test", + size = "small", + srcs = ["cached_log_test.cc"], + deps = [ + ":cached_log", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "affine_relation_test", + size = "small", + srcs = [ + "affine_relation_test.cc", + ], + deps = [ + ":affine_relation", + "//ortools/base:gmock_main", + ], +) + +cc_library( + name = "status_streaming", + srcs = ["status_streaming.cc"], + hdrs = ["status_streaming.h"], + visibility = ["//visibility:public"], + deps = [ + ":status_cc_proto", + "//ortools/base:source_location", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings:cord", + "@abseil-cpp//absl/strings:string_view", + ], +) + +cc_test( + name = "status_streaming_test", + srcs = ["status_streaming_test.cc"], + deps = [ + ":status_streaming", + "//ortools/base:gmock_main", + "//ortools/base:log_severity", + "//ortools/base:source_location", + "@abseil-cpp//absl/log:scoped_mock_log", + "@abseil-cpp//absl/status", + ], +) diff --git a/ortools/util/README b/ortools/util/README deleted file mode 100644 index e33c2e52792..00000000000 --- a/ortools/util/README +++ /dev/null @@ -1,7 +0,0 @@ - Various utilities to be used by libraries - -This directory contains various utilities needed by the operations -research libraries. - - - const_int_array.h : A read-only container of integer values. - - const_ptr_array.h : A read-only container of pointers. diff --git a/ortools/util/adaptative_parameter_value_test.cc b/ortools/util/adaptative_parameter_value_test.cc new file mode 100644 index 00000000000..814fb01641b --- /dev/null +++ b/ortools/util/adaptative_parameter_value_test.cc @@ -0,0 +1,121 @@ +// 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/util/adaptative_parameter_value.h" + +#include + +#include "absl/random/random.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(AdaptiveParameterValueTest, StayBelowOne) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 1e6; ++i) param.Increase(); + EXPECT_GE(param.value(), 0.99); + EXPECT_LE(param.value(), 1.0); +} + +TEST(AdaptiveParameterValueTest, StayAboveZero) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 1e6; ++i) param.Decrease(); + EXPECT_GE(param.value(), 0.0); + EXPECT_LE(param.value(), 0.001); +} + +TEST(AdaptiveParameterValueTest, ConvergeToTarget) { + absl::BitGen random; + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 50; ++i) { + const double target = absl::Uniform(random, 0.0, 1.0); + for (int j = 0; j < 10000; ++j) { + if (param.value() < target) param.Increase(); + if (param.value() > target) param.Decrease(); + } + + // We expect convergence even after a lot of updates. + EXPECT_GE(param.value(), target - 0.1); + EXPECT_LE(param.value(), target + 0.1); + } +} + +TEST(AdaptiveParameterValueTest, ExponentialConvergenceToOne) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 100; ++i) param.Increase(); + EXPECT_GE(param.value(), 1.0 - 1e-6); +} + +TEST(AdaptiveParameterValueTest, ExponentialConvergenceToZero) { + AdaptiveParameterValue param(0.5); + for (int i = 0; i < 100; ++i) param.Decrease(); + EXPECT_LE(param.value(), 1e-6); +} + +TEST(AdaptiveParameterValueTest, ConvergenceToMedian) { + AdaptiveParameterValue param(0.5); + + // The test should work (modulo tolerances) for any increasing function f in + // [0, 1] with f(0) = 0 and f(1) = 1. + const auto f = [](double x) { return x * x; }; + + // The final value should be around f(param.value) = 0.5 ! + // The convergence is pretty slow though. + absl::BitGen random; + for (int i = 0; i < 1e6; ++i) { + const double decrease = absl::Bernoulli(random, f(param.value())); + if (decrease) { + param.Decrease(); + } else { + param.Increase(); + } + } + + // No flakiness in 1000 runs with this tolerance. + EXPECT_NEAR(param.value(), sqrt(0.5), 0.05); +} + +TEST(AdaptiveParameterValueTest, UpdateVsIncrease) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Increase(); + param1.Increase(); + param1.Increase(); + param2.Update(/*num_decreases=*/0, /*num_increases=*/3); + EXPECT_EQ(param1.value(), param2.value()); +} + +TEST(AdaptiveParameterValueTest, UpdateVsDecrease) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Decrease(); + param1.Decrease(); + param2.Update(/*num_decreases=*/2, /*num_increases=*/0); + EXPECT_EQ(param1.value(), param2.value()); +} + +TEST(AdaptiveParameterValueTest, GenericUpdate) { + AdaptiveParameterValue param1(0.1); + AdaptiveParameterValue param2(0.1); + param1.Decrease(); + param1.Increase(); + param1.Increase(); + param1.Decrease(); + param2.Update(/*num_decreases=*/2, /*num_increases=*/2); + EXPECT_EQ(param2.value(), 0.1); + EXPECT_NE(param1.value(), param2.value()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/affine_relation_test.cc b/ortools/util/affine_relation_test.cc new file mode 100644 index 00000000000..79c2dffdcb7 --- /dev/null +++ b/ortools/util/affine_relation_test.cc @@ -0,0 +1,70 @@ +// 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/util/affine_relation.h" + +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(AffineRelationTest, Empty) { + AffineRelation r; + EXPECT_EQ(r.Get(10), AffineRelation::Relation(10, 1, 0)); +} + +TEST(AffineRelationTest, CoeffOfMagnitudeOne) { + AffineRelation r; + r.TryAdd(1, 0, /*coeff=*/1, /*offset=*/1); // r1 = r0 + 1 + r.TryAdd(2, 0, /*coeff=*/1, /*offset=*/2); // r2 = r0 + 2 + r.TryAdd(3, 0, /*coeff=*/-1, /*offset=*/3); // r3 = -r0 + 3 + r.TryAdd(4, 0, /*coeff=*/-1, /*offset=*/4); // r4 = -r0 + 4 + r.TryAdd(5, 2, /*coeff=*/-1, /*offset=*/-3); // r5 = -r2 - 3 = -r0 - 5 + r.TryAdd(6, 3, /*coeff=*/-1, /*offset=*/-3); // r6 = -r3 - 3 = -r0 - 8 + r.TryAdd(7, 4, /*coeff=*/-1, /*offset=*/-3); // r7 = -r4 - 3 = r0 - 7 + + EXPECT_EQ(r.Get(0), AffineRelation::Relation(0, 1, 0)); + EXPECT_EQ(r.Get(1), AffineRelation::Relation(0, 1, 1)); + EXPECT_EQ(r.Get(2), AffineRelation::Relation(0, 1, 2)); + EXPECT_EQ(r.Get(3), AffineRelation::Relation(0, -1, 3)); + EXPECT_EQ(r.Get(4), AffineRelation::Relation(0, -1, 4)); + EXPECT_EQ(r.Get(5), AffineRelation::Relation(0, -1, -1 * 2 - 3)); + EXPECT_EQ(r.Get(6), AffineRelation::Relation(0, 1, -1 * 3 - 3)); + EXPECT_EQ(r.Get(7), AffineRelation::Relation(0, 1, -1 * 4 - 3)); +} + +TEST(AffineRelationTest, ChainOfMultiple) { + AffineRelation r; + EXPECT_TRUE(r.TryAdd(1, 0, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(2, 1, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(3, 2, /*coeff=*/2, /*offset=*/1)); + EXPECT_TRUE(r.TryAdd(4, 3, /*coeff=*/2, /*offset=*/1)); + + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(r.Get(i), AffineRelation::Relation(0, 1 << i, (1 << i) - 1)); + } + + EXPECT_TRUE(r.TryAdd(6, 5, /*coeff=*/3, /*offset=*/0)); + + // We can't add any of the following. + EXPECT_FALSE(r.TryAdd(1, 0, 2, 1)); + EXPECT_FALSE(r.TryAdd(3, 4, 2, 1)); + EXPECT_FALSE(r.TryAdd(1, 6, 1, 0)); + EXPECT_FALSE(r.TryAdd(6, 1, 1, 0)); + + // But we can connect the root of the chain. + EXPECT_TRUE(r.TryAdd(0, 6, 1, 0)); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/cached_log_test.cc b/ortools/util/cached_log_test.cc new file mode 100644 index 00000000000..431e3de47d4 --- /dev/null +++ b/ortools/util/cached_log_test.cc @@ -0,0 +1,36 @@ +// 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/util/cached_log.h" + +#include + +#include + +#include "gtest/gtest.h" + +namespace { + +TEST(CachedLogTest, LogAccess) { + const int kSize = 100; + operations_research::CachedLog cache; + for (int64_t i = 1; i < kSize; ++i) { + EXPECT_DOUBLE_EQ(log2(i), cache.Log2(i)); + } + cache.Init(kSize / 2); + for (int64_t i = 1; i < kSize; ++i) { + EXPECT_DOUBLE_EQ(log2(i), cache.Log2(i)); + } +} + +} // namespace diff --git a/ortools/util/dense_set_benchmark.cc b/ortools/util/dense_set_benchmark.cc new file mode 100644 index 00000000000..3d92b96b64c --- /dev/null +++ b/ortools/util/dense_set_benchmark.cc @@ -0,0 +1,100 @@ +// 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 +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "benchmark/benchmark.h" +#include "ortools/util/dense_set.h" + +namespace operations_research { +namespace { + +template +int64_t DoOneBMIteration(Set& set, absl::BitGenRef gen) { + int64_t sum = 0; + // Don't use generate random numbers inside the innermost loop to avoid + // benchmarking absl::Uniform. + int64_t base = absl::Uniform(gen, 0, 1024 * 1024); + for (int i = 1; i <= 100; ++i) { + set.insert((i * base) % (1024 * 1024)); + } + for (int v : set) { + sum += v; + } + while (set.size() > 1000) { + set.erase(set.begin()); + } + return sum; +} + +template +void BM_SetIteration(benchmark::State& state, Set set = Set()) { + absl::BitGen gen; + int64_t sum = 0; + int base = absl::Uniform(gen, 1, 1024 * 1024); + for (int i = 0; i < 1000; ++i) { + set.insert(i * base % (1024 * 1024)); + } + for (auto s : state) { + sum += DoOneBMIteration(set, gen); + } +} + +void BM_HashSetIteration(benchmark::State& state) { + BM_SetIteration>(state); +} +void BM_DenseSetIteration(benchmark::State& state) { + BM_SetIteration>(state); +} +void BM_HashSetIterationPreReserved(benchmark::State& state) { + absl::flat_hash_set set; + set.reserve(1500); + BM_SetIteration(state, std::move(set)); +} +void BM_DenseSetIterationPreReserved(benchmark::State& state) { + DenseSet set; + set.reserve(1024 * 1024); + BM_SetIteration(state, std::move(set)); +} +void BM_UnsafeDenseSetIterationPreReserved(benchmark::State& state) { + UnsafeDenseSet set; + set.reserve(1024 * 1024); + BM_SetIteration(state, std::move(set)); +} +void BM_HashSetIterationPreReservedRehash(benchmark::State& state) { + absl::flat_hash_set set; + set.reserve(1500); + absl::BitGen gen; + int64_t sum = 0; + for (int i = 0; i < 1000; ++i) { + set.insert(absl::Uniform(gen, 0, 1024 * 1024)); + } + for (auto s : state) { + set.rehash(1500); + sum += DoOneBMIteration(set, gen); + } +} + +BENCHMARK(BM_HashSetIteration); +BENCHMARK(BM_DenseSetIteration); +BENCHMARK(BM_HashSetIterationPreReserved); +BENCHMARK(BM_DenseSetIterationPreReserved); +BENCHMARK(BM_UnsafeDenseSetIterationPreReserved); +BENCHMARK(BM_HashSetIterationPreReservedRehash); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/dense_set_test.cc b/ortools/util/dense_set_test.cc new file mode 100644 index 00000000000..a4d3f25f4b8 --- /dev/null +++ b/ortools/util/dense_set_test.cc @@ -0,0 +1,151 @@ +// 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/util/dense_set.h" + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_int.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +DEFINE_STRONG_INT_TYPE(TestStrongInt, int); + +template +class DenseSetTest : public testing::Test {}; + +using SetTypes = + ::testing::Types, DenseSet, + UnsafeDenseSet, UnsafeDenseSet>; + +TYPED_TEST_SUITE(DenseSetTest, SetTypes); + +TYPED_TEST(DenseSetTest, TestInsert) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + + auto [it, inserted] = set.insert(static_cast(1)); + + EXPECT_TRUE(inserted); + EXPECT_TRUE(set.contains(static_cast(1))); + EXPECT_EQ(*it, static_cast(1)); + EXPECT_EQ(set.size(), 1); + EXPECT_THAT(set, ElementsAre(static_cast(1))); + EXPECT_THAT(set.values(), ElementsAre(static_cast(1))); +} + +TYPED_TEST(DenseSetTest, TestInsertDuplicate) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + + auto [it, inserted] = set.insert(static_cast(1)); + auto [it2, inserted2] = set.insert(static_cast(1)); + + EXPECT_TRUE(inserted); + EXPECT_FALSE(inserted2); + EXPECT_EQ(it, it2); +} + +TYPED_TEST(DenseSetTest, TestFindPresent) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + set.insert(static_cast(1)); + + auto it = set.find(static_cast(1)); + + EXPECT_EQ(*it, static_cast(1)); +} + +TYPED_TEST(DenseSetTest, TestFindAbsent) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(3); + set.insert(static_cast(1)); + + auto it = set.find(static_cast(2)); + + EXPECT_EQ(it, set.end()); +} + +TYPED_TEST(DenseSetTest, TestEraseValue) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2001); + set.insert(static_cast(1)); + + EXPECT_EQ(set.erase(static_cast(0)), 0); + EXPECT_EQ(set.erase(static_cast(2000)), 0); + EXPECT_TRUE(set.contains(static_cast(1))); + EXPECT_EQ(set.erase(static_cast(1)), 1); + EXPECT_FALSE(set.contains(static_cast(1))); + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestEraseIterator) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + auto [it, inserted] = set.insert(static_cast(1)); + + set.erase(it); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestClear) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(3); + set.insert(static_cast(1)); + set.insert(static_cast(2)); + + set.clear(); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); +} + +TYPED_TEST(DenseSetTest, TestReserve) { + TypeParam set; + + set.reserve(100); + + EXPECT_EQ(set.size(), 0); + EXPECT_THAT(set, IsEmpty()); + EXPECT_THAT(set.values(), IsEmpty()); + EXPECT_EQ(set.capacity(), 100); +} + +TYPED_TEST(DenseSetTest, TestConstIterators) { + TypeParam set; + if (!TypeParam::kAutoResize) set.reserve(2); + auto [it, inserted] = set.insert(static_cast(1)); + + // Check that `*it = TypeParam::value_type();` would not compile. + EXPECT_FALSE((std::is_assignable::value)); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/flat_matrix_benchmark.cc b/ortools/util/flat_matrix_benchmark.cc new file mode 100644 index 00000000000..0f857567297 --- /dev/null +++ b/ortools/util/flat_matrix_benchmark.cc @@ -0,0 +1,50 @@ +// 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 + +#include "absl/base/casts.h" +#include "absl/log/check.h" +#include "benchmark/benchmark.h" +#include "ortools/util/flat_matrix.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { +namespace { + +template +void BM_FlatMatrixRandomAccess(benchmark::State& state) { + const int size = state.range(0); + const uint64_t mask = size - 1; + CHECK(!(mask & size)) << "size must be a power of 2: " << size; + FlatMatrix m(size, size); + random_engine_t random; + double sum = 0; + for (const auto& _ : state) { + const uint64_t x = random(); + if (do_work) { + sum += m[x & mask][(x >> 32) & mask]; + } else { + // The 'baseline' timing does similar operations, without the matrix + // lookup. + sum += absl::bit_cast(x & ((mask << 32) | mask)); + } + } + benchmark::DoNotOptimize(sum); +} + +BENCHMARK(BM_FlatMatrixRandomAccess)->Range(1, 1 << 12); +BENCHMARK(BM_FlatMatrixRandomAccess)->Range(1, 1 << 12); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/flat_matrix_test.cc b/ortools/util/flat_matrix_test.cc new file mode 100644 index 00000000000..5697eb38bd3 --- /dev/null +++ b/ortools/util/flat_matrix_test.cc @@ -0,0 +1,215 @@ +// 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/util/flat_matrix.h" + +#include +#include +#include +#include +#include + +#include "absl/base/casts.h" +#include "absl/log/check.h" +#include "gtest/gtest.h" +#include "ortools/base/dump_vars.h" +#include "ortools/base/gmock.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +TEST(FlatMatrixTest, ConstructionAndSizeGetters) { + FlatMatrix m(2, 3); + EXPECT_EQ(m.num_rows(), 2); + EXPECT_EQ(m.num_cols(), 3); +} + +TEST(FlatMatrixTest, ZeroRows) { + FlatMatrix m(0, 42); + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 42); +} + +TEST(FlatMatrixTest, ZeroCols) { + FlatMatrix m(42, 0); + EXPECT_EQ(m.num_rows(), 42); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, ZeroRowsAndZeroCols) { + FlatMatrix m(0, 0); + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, DefaultConstructor) { + FlatMatrix m; + EXPECT_EQ(m.num_rows(), 0); + EXPECT_EQ(m.num_cols(), 0); +} + +TEST(FlatMatrixTest, ElementsAreZeroedAtConstruction) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + ASSERT_EQ(m[row][col], 0) << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, InitializeWithNonDefault) { + FlatMatrix m(5, 4, "Hello"); + for (int row = 0; row < 5; ++row) { + for (int col = 0; col < 4; ++col) { + ASSERT_EQ(m[row][col], "Hello") << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, WriteAndReadElements) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + EXPECT_EQ(m[row][col], 100 * row + 10 * col) << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, CopyAndMoveOperators) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + // We chain a copy and a move to reduce the amount of test code. + FlatMatrix copied_construction = m; + FlatMatrix copied_assigned(4, 5); + copied_assigned = m; + FlatMatrix moved_construction = std::move(copied_construction); + FlatMatrix moved_assigned(4, 5); + moved_assigned = std::move(copied_assigned); + + ASSERT_EQ(moved_construction.num_rows(), 2); + ASSERT_EQ(moved_construction.num_cols(), 3); + ASSERT_EQ(moved_assigned.num_rows(), 2); + ASSERT_EQ(moved_assigned.num_cols(), 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + ASSERT_EQ(moved_construction[row][col], 100 * row + 10 * col) + << DUMP_VARS(row, col); + ASSERT_EQ(moved_assigned[row][col], 100 * row + 10 * col) + << DUMP_VARS(row, col); + } + } +} + +TEST(FlatMatrixTest, MoveDoesPerformRealMove) { + // Move-construct a ~7M element matrix a million times: if it wasn't a move, + // it would be far too slow and need huge amounts of memory. + std::vector>> matrices(1'000'000); + matrices[0] = std::make_unique>(2345, 3456); + (*matrices[0])[2344][3455] = 42.42; + for (int i = 1; i < 1000000; ++i) { + matrices[i] = + std::make_unique>(std::move(*matrices[i - 1])); + } + EXPECT_EQ((*matrices.back())[2344][3455], 42.42); +} + +TEST(FlatMatrixTest, RowIteration) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + EXPECT_THAT(m[1], ElementsAre(100, 110, 120)); +} + +TEST(FlatMatrixTest, RowIterationOnEmptyRow) { + FlatMatrix m(3, 0); + EXPECT_THAT(m[2], IsEmpty()); +} + +TEST(FlatMatrixTest, AllElementsConst) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + const FlatMatrix const_m = m; + EXPECT_THAT(const_m.all_elements(), ElementsAre(0, 10, 20, 100, 110, 120)); +} + +TEST(FlatMatrixTest, AllElementsMutable) { + FlatMatrix m(2, 3); + int value = 10; + for (int& i : m.all_elements()) { + i = value++; + } + EXPECT_THAT(m.all_elements(), ElementsAre(10, 11, 12, 13, 14, 15)); +} + +TEST(FlatMatrixTest, AllElementsInlineMutation) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + for (int& i : m.all_elements()) { + i *= 10; + } + EXPECT_THAT(m.all_elements(), ElementsAre(0, 100, 200, 1000, 1100, 1200)); +} + +TEST(FlatMatrixTest, AllElementsEmptyMatrix) { + FlatMatrix m; + EXPECT_THAT(m.all_elements(), ElementsAre()); +} + +TEST(FlatMatrixTest, RowIterator) { + FlatMatrix m(2, 3); + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 3; ++col) { + m[row][col] = 100 * row + 10 * col; + } + } + EXPECT_THAT(m.rows(), + ElementsAre(ElementsAre(0, 10, 20), ElementsAre(100, 110, 120))); +} + +TEST(FlatMatrixTest, RowIteratorOnEmptyMatrix) { + FlatMatrix m; + EXPECT_THAT(m.rows(), ElementsAre()); +} + +TEST(FlatMatrixTest, RowIteratorOnZeroColumnMatrix) { + FlatMatrix m(3, 0); + EXPECT_THAT(m.rows(), + ElementsAre(ElementsAre(), ElementsAre(), ElementsAre())); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_roundtrip_conv_testing_test.cc b/ortools/util/fp_roundtrip_conv_testing_test.cc new file mode 100644 index 00000000000..b9450248979 --- /dev/null +++ b/ortools/util/fp_roundtrip_conv_testing_test.cc @@ -0,0 +1,39 @@ +// 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/util/fp_roundtrip_conv_testing.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/util/fp_roundtrip_conv.h" + +namespace operations_research { +namespace { + +TEST(TestNumberTest, Test) { + { + std::ostringstream oss; + oss << kRoundTripTestNumber; + EXPECT_NE(oss.str(), kRoundTripTestNumberStr); + EXPECT_EQ(oss.str(), "0.1"); + } + { + std::ostringstream oss; + oss << RoundTripDoubleFormat(kRoundTripTestNumber); + EXPECT_EQ(oss.str(), kRoundTripTestNumberStr); + } +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_utils_benchmark.cc b/ortools/util/fp_utils_benchmark.cc new file mode 100644 index 00000000000..915b4947ba1 --- /dev/null +++ b/ortools/util/fp_utils_benchmark.cc @@ -0,0 +1,195 @@ +// 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 + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "benchmark/benchmark.h" +#include "ortools/util/fp_utils.h" + +namespace operations_research { +namespace { + +void BM_fast_ilogb(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += fast_ilogb(values[i]); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_fast_ilogb); + +void BM_Ilogb(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += ilogb(values[i]); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_Ilogb); + +void BM_frexp(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + const double value = values[i]; + int exponent; + if (value == 0.0) { + exponent = FP_ILOGB0; + } else { + (void)frexp(values[i], &exponent); + } + total += exponent - 1; + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_frexp); + +void BM_log2(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(exp2(i - 512)); + } + + for (auto s : state) { + int total = 0; + for (int i = 0; i < length; ++i) { + total += log2(std::abs(values[i])); + } + CHECK_EQ(total, (length * (length - 1) / 2 - 512 * length)); + } +} +BENCHMARK(BM_log2); + +// To scale numbers with pure power of two exponents the code relies on scalbn, +// the alternatives are ldexp and exp2. We benchmark here the alternatives to +// ensure scalbn is indeed faster. +void BM_fast_scalbn(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += fast_scalbn(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_fast_scalbn); + +void BM_scalbn(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += scalbn(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_scalbn); + +void BM_ldexp(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += ldexp(1.0, values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_ldexp); + +void BM_exp2(benchmark::State& state) { + const int length = 1024; + std::vector values; + values.reserve(length); + for (int i = 0; i < length; ++i) { + values.push_back(i - 512); + } + + for (auto s : state) { + double total = 0; + for (int i = 0; i < length; ++i) { + total += 1.0 * exp2(values[i]); + } + CHECK_EQ(total, 0x1.ffffffffffffffp511); + } +} +BENCHMARK(BM_exp2); + +// (Generated by http://go/benchy. Settings: --runs 20 --guitar_cluster +// chamber-experiments-guitar3 //ortools/util:fp_utils_test) +// +// name time/op +// BM_fast_ilogb 1.95µs ± 1% +// BM_Ilogb 3.58µs ± 3% +// BM_frexp 3.51µs ± 1% +// BM_log2 7.74µs ± 1% +// BM_fast_scalbn 3.47µs ± 1% +// BM_scalbn 3.89µs ± 2% +// BM_ldexp 6.19µs ± 1% +// BM_exp2 6.19µs ± 2% + +} // namespace +} // namespace operations_research diff --git a/ortools/util/fp_utils_test.cc b/ortools/util/fp_utils_test.cc new file mode 100644 index 00000000000..8d1a12d029b --- /dev/null +++ b/ortools/util/fp_utils_test.cc @@ -0,0 +1,503 @@ +// 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/util/fp_utils.h" + +#include + +#include // NOLINT(build/c++11) +#include +#include +#include +#include +#include +#include + +#include "absl/base/casts.h" +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/random/random.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" + +namespace operations_research { +namespace { + +// To have more readable one-liners below. +const double kInfinity = std::numeric_limits::infinity(); + +// An intricate way to set 'result' to zero, to avoid compiler optimization. +double GenerateZero() { + const int n = 100; + double result = n * (n - 1) / 2; + for (int i = 0; i < n; ++i) { + result -= i; + } + CHECK_EQ(0.0, result); + return result; +} + +double GenerateNaN() { return GenerateZero() / GenerateZero(); } + +double GenerateDivisionByZero() { return 1.0 / GenerateZero(); } + +void RunFloatingPointExceptionTest(int exception_flags, bool detect_nan, + bool detect_div_by_zero) { + ScopedFloatingPointEnv scoped_fenv; + if (!scoped_fenv.EnableExceptions(exception_flags)) { + GTEST_SKIP() << "Floating point exceptions are not supported."; + } + if (detect_nan) { + EXPECT_DEATH( + { + const double x = GenerateNaN(); + EXPECT_NE(x, x); // Use the value to prevent removal by optimization. + }, + ""); + } else { + const double x = GenerateNaN(); + EXPECT_NE(x, x); + } + if (detect_div_by_zero) { + EXPECT_DEATH( + { + // Use the value to prevent removal by optimization. + EXPECT_EQ(kInfinity, GenerateDivisionByZero()); + }, + ""); + } else { + EXPECT_EQ(kInfinity, GenerateDivisionByZero()); + } +} + +TEST(ScopedFpEnv, NoDetection) { + RunFloatingPointExceptionTest(0, false, false); +} + +TEST(ScopedFpEnv, NanDetection) { + RunFloatingPointExceptionTest(FE_INVALID, true, false); +} + +TEST(ScopedFpEnv, DivisionByZeroDetection) { + RunFloatingPointExceptionTest(FE_DIVBYZERO, false, true); +} + +TEST(ScopedFpEnv, NanDetectionDivisionByZeroDetection) { + RunFloatingPointExceptionTest(FE_INVALID | FE_DIVBYZERO, true, true); +} + +TEST(Comparable, ComparisonsWithinTolerances) { + EXPECT_COMPARABLE(-kInfinity, -kInfinity, 1e-6); + EXPECT_COMPARABLE(kInfinity, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(-kInfinity, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(kInfinity, -kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, kInfinity, 1e-6); + EXPECT_NOTCOMPARABLE(-kInfinity, 1.0, 1e-6); + EXPECT_COMPARABLE(1.0, 1.0 + 1e-7, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, 1.0 + 1e-5, 1e-6); + EXPECT_COMPARABLE(0.0, 0.0 + 1e-7, 1e-6); + EXPECT_NOTCOMPARABLE(0.0, 0.0 + 1e-5, 1e-6); + EXPECT_NOTCOMPARABLE(1.0, std::numeric_limits::quiet_NaN(), 1e-6); +} + +TEST(Comparable, ComparisonsWithinAbolusteTolerance) { + EXPECT_TRUE(AreWithinAbsoluteTolerance(-kInfinity, -kInfinity, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(-kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(kInfinity, -kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(1.0, kInfinity, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(-kInfinity, 1.0, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(1.0, 1.0 + 1e-7, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(1.0, 1.0 + 1e-5, 1e-6)); + EXPECT_TRUE(AreWithinAbsoluteTolerance(0.0, 0.0 + 1e-7, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance(0.0, 0.0 + 1e-5, 1e-6)); + EXPECT_FALSE(AreWithinAbsoluteTolerance( + 1.0, std::numeric_limits::quiet_NaN(), 1e-6)); +} + +TEST(IsSmallerWithinToleranceTest, BasicTest) { + EXPECT_TRUE(IsSmallerWithinTolerance(kInfinity, kInfinity, 1e-6)); + EXPECT_TRUE(IsSmallerWithinTolerance(-kInfinity, -kInfinity, 1e-6)); + EXPECT_TRUE(IsSmallerWithinTolerance(-kInfinity, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, -kInfinity, 1e-6)); + + EXPECT_TRUE(IsSmallerWithinTolerance(123.456, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, 4567.0, 1e-6)); + + const double kNaN = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, -kInfinity, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kNaN, 0.0, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(kInfinity, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(-kInfinity, kNaN, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(0.0, kNaN, 1e-6)); + + EXPECT_TRUE(IsSmallerWithinTolerance(-5.0 + 1e-6, -5.0, 1e-6)); + EXPECT_FALSE(IsSmallerWithinTolerance(-5.0 + 1e-6, -5.0, 1e-8)); + EXPECT_TRUE(IsSmallerWithinTolerance(1.0, 1.0, 0.0)); + EXPECT_FALSE(IsSmallerWithinTolerance( + 1.0 + std::numeric_limits::epsilon(), 1.0, 0.0)); +} + +TEST(IsIntegerWithinToleranceTest, BasicTest) { + EXPECT_TRUE(IsIntegerWithinTolerance(4.9, .2)); + EXPECT_FALSE(IsIntegerWithinTolerance(4.9, .05)); + + EXPECT_TRUE(IsIntegerWithinTolerance(5.1, .2)); + EXPECT_FALSE(IsIntegerWithinTolerance(5.1, .05)); +} + +TEST(IsIntegerWithinToleranceTest, InfTest) { + EXPECT_FALSE(IsIntegerWithinTolerance(kInfinity, 100.0)); + EXPECT_FALSE(IsIntegerWithinTolerance(-kInfinity, 100.0)); + EXPECT_FALSE(IsIntegerWithinTolerance(kInfinity, kInfinity)); + EXPECT_FALSE(IsIntegerWithinTolerance(-kInfinity, kInfinity)); +} + +TEST(GetBestScalingOfDoublesToInt64Test, ErrorCases) { + const double kInfinity = std::numeric_limits::infinity(); + std::vector input{0, 1, 2, 3}; + double scale; + double error; + + input[1] = kInfinity; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = -kInfinity; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = std::numeric_limits::quiet_NaN(); + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + input[1] = std::numeric_limits::signaling_NaN(); + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, kInfinity); + + // Just to show that it works with 0. + input[1] = 0; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, 0); + + // And note that the max_absolute_sum negative also lead to an error. + GetBestScalingOfDoublesToInt64(input, -1, &scale, &error); + EXPECT_EQ(error, kInfinity); +} + +TEST(GetBestScalingOfDoublesToInt64Test, AllZeroVector) { + std::vector input(1000, 0.0); + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, uint64_t{1} << 60, &scale, &error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(scale, 1.0); +} + +TEST(GetBestScalingOfDoublesToInt64Test, ZeroBounds) { + std::vector input(1000, 50.0); + std::vector lb(1000, 0.0); + std::vector ub(1000, 0.0); + const double scale = + GetBestScalingOfDoublesToInt64(input, lb, ub, uint64_t{1} << 60); + EXPECT_EQ(scale, 1.0); + + double error; + double abs_error; + ComputeScalingErrors(input, lb, ub, scale, &error, &abs_error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(abs_error, 0.0); +} + +TEST(GetBestScalingOfDoublesToInt64Test, PowerOfTwo) { + std::vector input{ldexp(1.0, 100), ldexp(-1.0, 10), ldexp(1.0, -10)}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, ldexp(1.0, 60 - 100)); + EXPECT_EQ(error, 1.0); // The last value just disappeared... +} + +TEST(GetBestScalingOfDoublesToInt64Test, MaxSum) { + std::vector input{3, -3, 5, 0, 8}; + double scale; + double error; + + // The sum of the input absolute values is 19. + GetBestScalingOfDoublesToInt64(input, 19, &scale, &error); + EXPECT_EQ(scale, 1.0); + EXPECT_EQ(error, 0.0); + + // If we use a lower value, then the number needs to be scaled down. + GetBestScalingOfDoublesToInt64(input, 18, &scale, &error); + EXPECT_EQ(scale, 0.5); + + // We loose the bit at 1 from the 3s and 5. + EXPECT_COMPARABLE(error, 1.0 / 3.0, 1e-10); +} + +void CheckNoError(absl::Span input, double scale) { + for (double x : input) { + EXPECT_EQ(round(x * scale) / scale, x); + } +} + +TEST(GetBestScalingOfDoublesToInt64Test, NoErrorForIntegers) { + const int64_t max_mantissa = (1LL << std::numeric_limits::digits) - 1; + std::vector input{max_mantissa, 1, 123456789, 0, -max_mantissa, -10}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, 32.0); // This depends on the limit 1LL << 60. + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); + + // Note that if the exponent of all number are shifted, then this still work. + for (int i = 0; i < input.size(); ++i) { + input[i] = ldexp(input[i], -123); + } + GetBestScalingOfDoublesToInt64(input, int64_t{1} << 60, &scale, &error); + EXPECT_EQ(scale, ldexp(32.0, 123)); + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); +} + +TEST(GetBestScalingOfDoublesToInt64Test, NoErrorForCloseDoubles) { + std::mt19937 random(12345); + std::vector input; + // Because std::numeric_limits::digits is 53, we can represent + // them exactly with a scale of 2^52, and their sum will still stay below + // the specified limit (because we just sum 2^10 of them). + for (int i = 0; i < 1024; ++i) { + input.push_back(absl::Uniform(random, 1.0, 2.0)); + } + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, 0.0); + EXPECT_EQ(scale, int64_t{1} << 52); + CheckNoError(input, scale); + + // Now, if we sum some more, then we will need a lower scale and we will loose + // one bit of precision. + for (int i = 0; i < 1024; ++i) { + input.push_back(-absl::Uniform(random, 1.0, 2.0)); + } + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, std::numeric_limits::epsilon()); + EXPECT_EQ(scale, int64_t{1} << 51); +} + +TEST(GetBestScalingOfDoublesToInt64Test, BoundOnSumIsCorrect) { + std::mt19937 random(12345); + const double x = static_cast((1LL << 32) - 1); + std::vector input{1.0, x - 1, ldexp(x, 32)}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + + // Scaling by 0.5 is not enough, because the sum will be kint64max + 1 + // as shown by the following 3 lines: + EXPECT_EQ(1, static_cast(round(input[0] * 0.5))); + EXPECT_EQ((int64_t{1} << 31) - 1, + static_cast(round(input[1] * 0.5))); + EXPECT_EQ(((int64_t{1} << 32) - 1) << 31, + static_cast(round(input[2] * 0.5))); + + EXPECT_GT(error, 0.0); + EXPECT_EQ(scale, 1.0 / 4.0); + + // If x[0] is zero, then we can scale everyting by 0.5 without error. + input[0] = 0; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_EQ(error, 0.0); + CheckNoError(input, scale); + EXPECT_EQ(scale, 0.5); +} + +TEST(GetBestScalingOfDoublesToInt64Test, Infinity) { + std::vector input{1e-313}; + double scale; + double error; + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + EXPECT_LT(scale, std::numeric_limits::infinity()); + EXPECT_EQ(scale, ldexp(1.0, std::numeric_limits::max_exponent - 1)); +} + +TEST(GetBestScalingOfDoublesToInt64Test, Robustness) { + std::mt19937 random(12345); + std::vector input; + double scale; + double error; + for (int i = 0; i < 1024; ++i) { + input.clear(); + + // Generate i "garbage" doubles. + for (int j = 0; j < i; ++j) { + input.push_back(absl::bit_cast(absl::Uniform(random))); + } + + // Verify that nothing crash and that the value seems reasonable. + GetBestScalingOfDoublesToInt64(input, kint64max, &scale, &error); + if (error != std::numeric_limits::infinity()) { + CHECK_GT(scale, 0); + CHECK_GE(error, 0); + CHECK_LE(error, 1); + } + } +} + +TEST(ComputeScalingErrorTest, BasicTest) { + std::vector coeffs = {1.1, 0.7, -1.3}; + std::vector lbs = {0, 0, 0}; + std::vector ubs = {10, 10, 10}; + double coeff_relative_error; + double sum_max_error; + ComputeScalingErrors(coeffs, lbs, ubs, /*scaling_factor=*/1, + &coeff_relative_error, &sum_max_error); + + // The biggest relative error is on the coeff 0.7 that is rounded to 1. + EXPECT_NEAR(coeff_relative_error, 0.3 / 0.7, 1e-10); + + // The maximum sum difference is obtained on {0, 10, 10} where the exact sum + // is (0.7 - 1.3) * 10 and the rounded one is (1 - 1) * 10. + EXPECT_NEAR(sum_max_error, 6.0, 1e-10); +} + +TEST(ComputeGcdOfRoundedDoublesTest, BasicTest) { + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({0.1, -0.2, 0.3}, 1.0)); + EXPECT_EQ(10, ComputeGcdOfRoundedDoubles({50, 0, 40, 60}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({7, 5, 25, 14}, 1.0)); + EXPECT_EQ(1, ComputeGcdOfRoundedDoubles({-7, -5, 25, -14}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({7, 0}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({0, 7}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({-7, 0}, 1.0)); + EXPECT_EQ(7, ComputeGcdOfRoundedDoubles({0, -7}, 1.0)); + EXPECT_EQ(18, ComputeGcdOfRoundedDoubles({1, 0, 2, 3}, 18.0)); + EXPECT_EQ(18, ComputeGcdOfRoundedDoubles({1, 0, 2, 3}, -18.0)); +} + +TEST(InterpolateTest, BasicTest) { + EXPECT_DOUBLE_EQ(1.8, Interpolate(2, 1, .8)); +} + +constexpr int kDoubleMinExponent = std::numeric_limits::min_exponent; +constexpr int kDoubleMaxExponent = std::numeric_limits::max_exponent; +TEST(FastIlogbTest, Correctness) { + absl::BitGen bitgen; + for (int j = 0; j < 1024; ++j) { + for (int i = kDoubleMinExponent; i < kDoubleMaxExponent; ++i) { + for (int sign : {-1, 1}) { + const double input = + sign * absl::Uniform(absl::IntervalClosedOpen, bitgen, 1.0, 2.0); + ASSERT_EQ(fast_ilogb(scalbn(input, i)), i); + } + } + } +} + +TEST(FastScalbnTest, Correctness) { + std::mt19937 bitgen(19560618); + for (int j = 0; j < 1024; ++j) { + for (int i = kDoubleMinExponent; i < kDoubleMaxExponent; ++i) { + for (int sign : {-1, 1}) { + const double input = sign * absl::Uniform(bitgen, 1.0, 2.0); + ASSERT_EQ(fast_scalbn(input, i), scalbn(input, i)); + } + } + } +} + +template +T RandomElementOf(std::mt19937& random, const std::vector& array) { + return array[absl::Uniform(random, 0, array.size())]; +} + +// Test on random input, ensure that for finite numbers whose result is +// representable, we get the correct result. +TEST(FastScalbnTest, DontFailOnRubbish) { + constexpr int kNumTests = (1 << 25); + std::mt19937 bitgen(19540820); + int64_t count_ok = 0; + int64_t count_nan = 0; + int64_t count_subnormal = 0; + int64_t count_zero = 0; + int64_t count_infinity = 0; + // We inject some special values that are worth testing, but that are too + // unlikely to be produced by a purely bit-uniform random double. + const std::vector kSpecialValues = {kInfinity, -kInfinity, 0.0, -0.0}; + for (int i = 0; i < kNumTests; ++i) { + const double input = (i & 0xff) + ? absl::bit_cast(absl::Uniform( + bitgen, 0, kuint64max)) + : RandomElementOf(bitgen, kSpecialValues); + const int add_to_exponent = + (i % 97 != 0) ? absl::Uniform(bitgen, kDoubleMinExponent - 1, + kDoubleMaxExponent + 2) + : absl::Uniform(bitgen, kint32min, kint32max); + // Note that no matter the input, the function will not crash, although + // the result may be undefined behavior. + const double result = fast_scalbn(input, add_to_exponent); + if (std::isnan(input)) { + ++count_nan; + } else if (std::isinf(input)) { + ++count_infinity; + } else if (input == 0.0) { + ++count_zero; + ASSERT_EQ(result, input); + } else if (!std::isnormal(input)) { + ++count_subnormal; + } else { + const int input_exponent = fast_ilogb(input); + const int result_exponent = input_exponent + add_to_exponent; + if (result_exponent < kDoubleMaxExponent && + result_exponent >= kDoubleMinExponent) { + // If input and output is representable, the result must be equal to + // the value returned by the standard function. + const double posix_result = scalbn(input, add_to_exponent); + ASSERT_EQ(result, posix_result) << absl::StrFormat( + "\nBit representations:\n\t%10s %016llx %.16g\n\t%10s %016llx " + "%.16g\n\t%10s %016llx %.16g\nExponents:\n\t%10s %10d\n\t%10s " + "%10d\n\t%10s %10d\nIterations: %d", + "input", absl::bit_cast(input), input, "fast", + absl::bit_cast(result), result, "posix", + absl::bit_cast(posix_result), posix_result, "input", + input_exponent, "result", result_exponent, "add_to", + add_to_exponent, i); + ++count_ok; + } + } + } + LOG(INFO) << absl::StrFormat( + "Input tested:\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% (count " + "%g)\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% (count %g)\n\t%20s %.4f%% " + "(count %g)", + "Valid", count_ok * 100.0 / kNumTests, count_ok, "Subnormal", + count_subnormal * 100.0 / kNumTests, count_subnormal, "NaN", + count_nan * 100.0 / kNumTests, count_nan, "Infinity", + count_infinity * 100.0 / kNumTests, count_infinity, "Zero", + count_zero * 100.0 / kNumTests, count_zero); + EXPECT_LT(0.55 * kNumTests, count_ok); + EXPECT_LT(4e-4 * kNumTests, count_subnormal); + EXPECT_LT(4e-4 * kNumTests, count_nan); + EXPECT_LT(4e-4 * kNumTests, count_infinity); + EXPECT_LT(4e-4 * kNumTests, count_zero); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/lazy_mutable_copy_test.cc b/ortools/util/lazy_mutable_copy_test.cc new file mode 100644 index 00000000000..c32d84fe1e2 --- /dev/null +++ b/ortools/util/lazy_mutable_copy_test.cc @@ -0,0 +1,245 @@ +// 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/util/lazy_mutable_copy.h" + +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +// An object that statically tracks the number of times its copy constructor +// was called. Not thread-safe at all, but for our purpose here it's fine. +class MyTestObject { + public: + explicit MyTestObject(absl::string_view data) : data_(data) {} + + MyTestObject(const MyTestObject& other) { + ++num_copies_; + data_ = other.data_; + } + + MyTestObject(MyTestObject&& other) { + ++num_moves_; + data_ = std::move(other.data_); + } + + ~MyTestObject() { ++num_deletes_; } + + const std::string& data() const { return data_; } + void set_data(absl::string_view data) { data_ = data; } + + static void ResetCounters() { + num_copies_ = 0; + num_moves_ = 0; + num_deletes_ = 0; + } + + static int num_copies_; + static int num_moves_; + static int num_deletes_; + + std::string data_; +}; + +// static +int MyTestObject::num_copies_ = 0; +int MyTestObject::num_moves_ = 0; +int MyTestObject::num_deletes_ = 0; + +TEST(LazyMutableCopyTest, SimpleOperations) { + // Create my baseline object. + MyTestObject::ResetCounters(); + MyTestObject obj("Hello"); + ASSERT_EQ(obj.data(), "Hello"); + + // Create a LazyMutableCopy. For now, don't mutate it: it doesn't copy. + LazyMutableCopy copy(obj); + ASSERT_FALSE(copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(copy->data(), "Hello"); + obj.set_data("World"); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(copy->data(), "World"); + ASSERT_FALSE(copy.has_ownership()); + + // Mutate it a first time: it performs a copy. + copy.get_mutable()->set_data("Foo"); + ASSERT_TRUE(copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(copy->data(), "Foo"); + EXPECT_EQ(obj.data(), "World"); // It did not affect the original object. + obj.set_data("Baz"); // Changing the original doesn't affect us. + EXPECT_EQ(copy->data(), "Foo"); + ASSERT_TRUE(copy.has_ownership()); + + // Mutate it a second time: no additional copy. + copy.get_mutable()->set_data("Bar"); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(copy->data(), "Bar"); + ASSERT_TRUE(copy.has_ownership()); +} + +TEST(LazyMutableCopyTest, MoveWorks) { + // We rely on segfaults and the heapchecker to detect duplicate deletes and + // memory leaks, respectively. + MyTestObject::ResetCounters(); + MyTestObject obj("Hello"); + + LazyMutableCopy initial_copy(obj); + ASSERT_FALSE(initial_copy.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(initial_copy->data(), "Hello"); + LazyMutableCopy moved0 = std::move(initial_copy); + ASSERT_FALSE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(moved0->data(), "Hello"); + obj.set_data("World"); + ASSERT_FALSE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(moved0->data(), "World"); + + // Now, make it mutable, and move it again. + moved0.get_mutable()->set_data("Foo"); + ASSERT_TRUE(moved0.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved0->data(), "Foo"); + LazyMutableCopy moved1 = std::move(moved0); + ASSERT_TRUE(moved1.has_ownership()); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved1->data(), "Foo"); + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(moved1->data(), "Foo"); + + // At destruction, nothing bad should happen. +} + +TEST(LazyMutableCopyTest, ReferenceAndClear) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(a); + EXPECT_EQ(&*wrapper, &a); // Same object. + EXPECT_EQ(&*wrapper, &a); // Still same object. + + // Does nothing, but still nullptr after. + std::move(wrapper).dispose(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 0); + EXPECT_EQ(MyTestObject::num_deletes_, 0); +} + +TEST(LazyMutableCopyTest, MoveAndClear) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(std::move(a)); + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // This destroys the moved object. + std::move(wrapper).dispose(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, ExtractAsUniquePtr) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(std::move(a)); + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // This destroys the moved object at the end, still no copy. + std::unique_ptr up = + std::move(wrapper).copy_or_move_as_unique_ptr(); + + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get(), nullptr); + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(wrapper.get_mutable(), nullptr); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, ReferenceAndCopy) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + LazyMutableCopy wrapper(a); + EXPECT_EQ(&*wrapper, &a); // Same object. + EXPECT_EQ(&*wrapper, &a); // Still same object. + + wrapper.get_mutable()->set_data("hello"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(wrapper->data(), "hello"); + } + + EXPECT_EQ(MyTestObject::num_copies_, 1); + EXPECT_EQ(MyTestObject::num_moves_, 0); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +TEST(LazyMutableCopyTest, MoveAndCopy) { + MyTestObject::ResetCounters(); + MyTestObject a("1234"); + + { + // Test that we can move the wrapper. + LazyMutableCopy w1(std::move(a)); + LazyMutableCopy wrapper(std::move(w1)); + + EXPECT_EQ(wrapper->data(), "1234"); + EXPECT_NE(&*wrapper, &a); // different object. + EXPECT_EQ(MyTestObject::num_moves_, 1); + + // We can mutate it without issue and not further work. + wrapper.get_mutable()->set_data("hello"); + EXPECT_EQ(wrapper->data(), "hello"); + wrapper.get_mutable()->set_data("other"); + EXPECT_EQ(wrapper->data(), "other"); + } + + EXPECT_EQ(MyTestObject::num_copies_, 0); + EXPECT_EQ(MyTestObject::num_moves_, 1); + EXPECT_EQ(MyTestObject::num_deletes_, 1); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/logging_test.cc b/ortools/util/logging_test.cc new file mode 100644 index 00000000000..f4157d5cbaa --- /dev/null +++ b/ortools/util/logging_test.cc @@ -0,0 +1,130 @@ +// 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/util/logging.h" + +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/numbers.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/port/scoped_std_stream_capture.h" + +namespace operations_research { +namespace { + +TEST(FormatCounterTest, BasicCases) { + EXPECT_EQ("12", FormatCounter(12)); + EXPECT_EQ("12'345", FormatCounter(12345)); + EXPECT_EQ("123'456'789", FormatCounter(123456789)); +} + +TEST(LoggingTest, StandardLogToStdout) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(true); + + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + SOLVER_LOG(&logger, "output to log"); + if (ScopedStdStreamCapture::kIsSupported) { + EXPECT_EQ(std::move(stdout_capture).StopCaptureAndReturnContents(), + "output to log\n"); + } +} + +TEST(LoggingTest, StandardLogDisabledStdout) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(false); + + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + SOLVER_LOG(&logger, "output to log"); + EXPECT_EQ(std::move(stdout_capture).StopCaptureAndReturnContents(), ""); +} + +TEST(LoggingTest, CaptureLogCallback) { + SolverLogger logger; + logger.EnableLogging(true); + logger.SetLogToStdOut(false); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 0); + + // Custom callback method. + std::string output_string; + const auto write_to_string = [&output_string](absl::string_view message) { + output_string.append(message); + output_string.append("\n"); + }; + logger.AddInfoLoggingCallback(write_to_string); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 1); + + SOLVER_LOG(&logger, "output to string"); + EXPECT_EQ(output_string, "output to string\n"); + SOLVER_LOG(&logger, "!!"); + EXPECT_EQ(output_string, "output to string\n!!\n"); + + logger.ClearInfoLoggingCallbacks(); + + CHECK_EQ(logger.NumInfoLoggingCallbacks(), 0); + SOLVER_LOG(&logger, "we should not see that"); + EXPECT_EQ(output_string, "output to string\n!!\n"); +} + +TEST(LoggingTest, Throttling) { + SolverLogger logger; + + int num_seen = 0; + int num_skipped = 0; + const auto callback = [&num_seen, &num_skipped](absl::string_view message) { + ++num_seen; + + const std::string marker = "[skipped_logs="; + const int start_pos = message.find(marker); + if (start_pos < 0) return; + int num = 0; + + std::string sub = std::string(message.substr(start_pos + marker.size())); + int end_pos = sub.find(']'); + if (end_pos < 0) return; + sub = sub.substr(0, end_pos); + + if (absl::SimpleAtoi(sub, &num)) num_skipped += num; + }; + + logger.EnableLogging(true); + logger.AddInfoLoggingCallback(callback); + + // Hopefully, this will be above the default limit in all settings. + const int id = logger.GetNewThrottledId(); + const int num_emitted = 1000; + for (int i = 0; i < num_emitted; ++i) { + logger.ThrottledLog(id, "test"); + if (i == 500) { + // Lets output some in the middle. + logger.FlushPendingThrottledLogs(/*ignore_rates=*/true); + } + } + + // Make sure we output last one. + logger.FlushPendingThrottledLogs(/*ignore_rates=*/true); + + EXPECT_GT(num_seen, 10); // We have a minimum limit. + EXPECT_LT(num_seen, 500); + EXPECT_EQ(num_seen + num_skipped, num_emitted); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/permutation_test.cc b/ortools/util/permutation_test.cc new file mode 100644 index 00000000000..562065b24da --- /dev/null +++ b/ortools/util/permutation_test.cc @@ -0,0 +1,35 @@ +// 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/util/permutation.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research { + +TEST(PermutationTest, BasicOperation) { + auto data = std::to_array({1005, 1000, 1004, 1001, 1003, 1002}); + auto permutation = std::to_array({1, 3, 5, 4, 2, 0}); + + ArrayIndexCycleHandler handler(data.data()); + PermutationApplier permuter(&handler); + static_assert(data.size() == permutation.size()); + permuter.Apply(permutation.data(), 0, data.size()); + for (int i = 0; i < data.size(); ++i) { + EXPECT_EQ(data[i], 1000 + i); + } +} + +} // namespace operations_research diff --git a/ortools/util/random_engine_test.cc b/ortools/util/random_engine_test.cc new file mode 100644 index 00000000000..6ca875853a4 --- /dev/null +++ b/ortools/util/random_engine_test.cc @@ -0,0 +1,27 @@ +// 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/util/random_engine.h" + +#include + +#include "gtest/gtest.h" + +// This test is just here to make sure that a type has been selected that +// models UniformRandomBitGenerator. +TEST(RandomEngineCompileTest, TestIt) { + operations_research::random_engine_t engine; + engine.seed(10); + ASSERT_LT(engine.min(), engine.max()); + EXPECT_LT(-1, std::uniform_int_distribution(0, 1)(engine)); +} diff --git a/ortools/util/range_query_function_test.cc b/ortools/util/range_query_function_test.cc new file mode 100644 index 00000000000..dbaecb8967a --- /dev/null +++ b/ortools/util/range_query_function_test.cc @@ -0,0 +1,238 @@ +// 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/util/range_query_function.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" + +namespace operations_research { +namespace { + +struct InsideIntervalTest { + int64_t range_begin; + int64_t range_end; + int64_t interval_begin; + int64_t interval_end; + int64_t first_inside_the_interval; + int64_t last_inside_the_interval; +}; + +struct TestCase { + std::function f; + int64_t domain_start; + int64_t domain_end; + std::vector inside_interval_tests; +}; + +class RangeQueryFunctionTest : public testing::Test { + protected: + void RunTests(const RangeIntToIntFunction* f, const TestCase& test); + static const struct TestCase kTests[]; +}; + +const TestCase RangeQueryFunctionTest::kTests[] = { + { + // TODO(user): add more inside_interval_tests + [](int64_t x) -> int64_t { return x; }, + /*domain_start=*/12, + /*domain_end=*/17, + /*inside_interval_tests=*/ + { + {14, 16, 15, 16, 15, 15}, + {12, 13, 12, 13, 12, 12}, + }, + }, + { + [](int64_t x) -> int64_t { return x % 4; }, + /*domain_start=*/11, + /*domain_end=*/16, + /*inside_interval_tests=*/ + { + {11, 16, 1, 3, 13, 14}, + }, + }, + { + [](int64_t x) -> int64_t { return 1000. * sin(x); }, + /*domain_start=*/-3, + /*domain_end=*/3, + /*inside_interval_tests=*/{}, + }, + { + [](int64_t x) -> int64_t { + const double value = sin(x) * cosh(x) * exp(x); + if (value > kint64max) return kint64max; + if (value < kint64min) return kint64min; + return value; + }, + /*domain_start=*/1153, + /*domain_end=*/1157, + /*inside_interval_tests=*/{}, + }, +}; + +void RangeQueryFunctionTest::RunTests(const RangeIntToIntFunction* f, + const TestCase& test) { + for (int64_t i = test.domain_start; i < test.domain_end; ++i) { + EXPECT_EQ(test.f(i), f->Query(i)); + } + + for (int64_t from = test.domain_start; from < test.domain_end; ++from) { + for (int64_t to = from + 1; to <= test.domain_end; ++to) { + int64_t brute_force_min = kint64max; + int64_t brute_force_max = kint64min; + for (int64_t i = from; i < to; ++i) { + brute_force_min = std::min(brute_force_min, test.f(i)); + brute_force_max = std::max(brute_force_max, test.f(i)); + } + EXPECT_EQ(brute_force_min, f->RangeMin(from, to)); + EXPECT_EQ(brute_force_max, f->RangeMax(from, to)); + } + } + + for (const InsideIntervalTest& inside_interval_test : + test.inside_interval_tests) { + EXPECT_EQ(inside_interval_test.first_inside_the_interval, + f->RangeFirstInsideInterval(inside_interval_test.range_begin, + inside_interval_test.range_end, + inside_interval_test.interval_begin, + inside_interval_test.interval_end)); + EXPECT_EQ(inside_interval_test.last_inside_the_interval, + f->RangeLastInsideInterval(inside_interval_test.range_begin, + inside_interval_test.range_end, + inside_interval_test.interval_begin, + inside_interval_test.interval_end)); + } +} + +TEST_F(RangeQueryFunctionTest, BareIntToIntFunction) { + for (const TestCase& test : kTests) { + std::unique_ptr f(MakeBareIntToIntFunction(test.f)); + RunTests(f.get(), test); + } +} + +TEST_F(RangeQueryFunctionTest, CachedIntToIntFunction) { + for (const TestCase& test : kTests) { + std::unique_ptr f( + MakeCachedIntToIntFunction(test.f, test.domain_start, test.domain_end)); + RunTests(f.get(), test); + } +} + +// The index rmq test is responsible for testing extreme sequences of values. +// This test should rather check if RangeMinMaxIndexFunction correctly calls the +// rmq, if functions with positive/negative domains work and the behaviour when +// the range is invalid. +class RangeMinMaxIndexFunctionTest : public testing::Test { + protected: + static int64_t CorrectResultsTestFunction(const int64_t arg) { + static const auto kValues = std::to_array({6, 8, -1, 3, 6}); + static const int64_t kOffset = 2; + CHECK(arg + kOffset < kValues.size()); + return kValues[arg + kOffset]; + } + static int64_t Identity(int64_t arg) { return arg; } +}; + +TEST_F(RangeMinMaxIndexFunctionTest, CorrectResults) { + std::unique_ptr f( + MakeCachedRangeMinMaxIndexFunction(CorrectResultsTestFunction, -2, 3)); + EXPECT_EQ(0, f->RangeMinArgument(-2, 3)); + EXPECT_EQ(-1, f->RangeMaxArgument(-2, 3)); + EXPECT_EQ(-2, f->RangeMinArgument(-2, 0)); + EXPECT_EQ(-1, f->RangeMaxArgument(-2, 0)); + EXPECT_EQ(1, f->RangeMinArgument(1, 3)); + EXPECT_EQ(2, f->RangeMaxArgument(1, 3)); +} + +TEST_F(RangeMinMaxIndexFunctionTest, DomainCheck) { + const std::function Identity = [](int64_t x) -> int64_t { + return x; + }; + { + std::unique_ptr negative_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, -200, -100)); + for (int64_t range_start : {-160, -150, -180}) { + for (int64_t range_end : {-140, -149, -100}) { + EXPECT_EQ(range_start, + negative_domain->RangeMinArgument(range_start, range_end)); + EXPECT_EQ(range_end - 1, + negative_domain->RangeMaxArgument(range_start, range_end)); + } + } + } + { + std::unique_ptr positive_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, 100, 200)); + for (int64_t range_start : {140, 150, 100}) { + for (int64_t range_end : {160, 151, 180}) { + EXPECT_EQ(range_start, + positive_domain->RangeMinArgument(range_start, range_end)); + EXPECT_EQ(range_end - 1, + positive_domain->RangeMaxArgument(range_start, range_end)); + } + } + } + { + std::unique_ptr kint64min_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, kint64min, kint64min + 1)); + EXPECT_EQ(kint64min, + kint64min_domain->RangeMinArgument(kint64min, kint64min + 1)); + EXPECT_EQ(kint64min, + kint64min_domain->RangeMaxArgument(kint64min, kint64min + 1)); + + std::unique_ptr kint64max_domain( + MakeCachedRangeMinMaxIndexFunction(Identity, kint64max - 1, kint64max)); + EXPECT_EQ(kint64max - 1, + kint64max_domain->RangeMinArgument(kint64max - 1, kint64max)); + EXPECT_EQ(kint64max - 1, + kint64max_domain->RangeMaxArgument(kint64max - 1, kint64max)); + } + EXPECT_DEATH(MakeCachedRangeMinMaxIndexFunction(Identity, 0, 0), + "Check failed: [^ ]*"); +} + +#ifndef NDEBUG +// RangeMinArgument and RangeMaxArgument are performance citical functions, and +// they are guarded with DCHECKs. Their behavior is undefined in release mode, +// so we check debug mode only. +TEST_F(RangeMinMaxIndexFunctionTest, InvalidRangeQuery) { + std::unique_ptr f( + MakeCachedRangeMinMaxIndexFunction(Identity, 0, 10)); + EXPECT_DEATH(f->RangeMinArgument(-1, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(0, 11), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(0, 0), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(5, 5), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(10, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMinArgument(-1, -20), "Check failed: [^ ]*"); + + EXPECT_DEATH(f->RangeMaxArgument(-1, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(0, 11), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(0, 0), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(5, 5), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(10, 10), "Check failed: [^ ]*"); + EXPECT_DEATH(f->RangeMaxArgument(-1, -20), "Check failed: [^ ]*"); +} +#endif // NDEBUG +} // namespace +} // namespace operations_research diff --git a/ortools/util/rational_approximation_test.cc b/ortools/util/rational_approximation_test.cc new file mode 100644 index 00000000000..8325011bb81 --- /dev/null +++ b/ortools/util/rational_approximation_test.cc @@ -0,0 +1,54 @@ +// 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/util/rational_approximation.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/util/fp_utils.h" + +namespace operations_research { +namespace { +static const double kEpsilon = std::numeric_limits::epsilon(); + +TEST(RationalApproximation, ContinuedFraction) { + Fraction fraction = RationalApproximation(2.0 / 3, kEpsilon); + EXPECT_EQ(fraction.first, 2); + EXPECT_EQ(fraction.second, 3); + const double kTestedNumber = 17.373281721478865; + fraction = RationalApproximation(kTestedNumber, kEpsilon); + // The result for fraction.first and fraction.second varies depending on the + // type for Fractional (kTestedNumber cannot be represented accurately + // in a float), so we just test that the fraction is close enough. + EXPECT_COMPARABLE(kTestedNumber, + static_cast(fraction.first) / + static_cast(fraction.second), + kEpsilon); + fraction = RationalApproximation(0.4214, kEpsilon); + EXPECT_EQ(fraction.first, 2107); + EXPECT_EQ(fraction.second, 5000); + fraction = RationalApproximation(0.42139999999999994, kEpsilon); + EXPECT_EQ(fraction.first, 2107); + EXPECT_EQ(fraction.second, 5000); + // Expects rational approximations within a given precision. + fraction = RationalApproximation(0.66667, 1e-5); + EXPECT_EQ(fraction.first, 2); + EXPECT_EQ(fraction.second, 3); + fraction = RationalApproximation(0.1428572, 1e-6); + EXPECT_EQ(fraction.first, 1); + EXPECT_EQ(fraction.second, 7); +} + +} // anonymous namespace +} // namespace operations_research diff --git a/ortools/util/rev_test.cc b/ortools/util/rev_test.cc new file mode 100644 index 00000000000..4828e333f90 --- /dev/null +++ b/ortools/util/rev_test.cc @@ -0,0 +1,230 @@ +// 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/util/rev.h" + +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/random/distributions.h" +#include "gtest/gtest.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace { + +TEST(RevRepositoryTest, BasicNonStampedBehavior) { + RevRepository repo; + int a = 1; + int b = 2; + int c = 3; + repo.SaveState(&a); // no-op. + a = 2; + repo.SetLevel(0); // no-op. + EXPECT_EQ(a, 2); + repo.SetLevel(1); + repo.SaveState(&a); + repo.SaveState(&b); + b = 13; + repo.SaveState(&b); + repo.SaveState(&c); + a = 12; + c = 14; + repo.SetLevel(10); + repo.SaveState(&a); + a = 20; + repo.SetLevel(11); + EXPECT_EQ(a, 20); + repo.SetLevel(10); + EXPECT_EQ(a, 20); + repo.SetLevel(9); + EXPECT_EQ(a, 12); + repo.SetLevel(1); + EXPECT_EQ(a, 12); + EXPECT_EQ(b, 13); + EXPECT_EQ(c, 14); + repo.SetLevel(0); + EXPECT_EQ(a, 2); + EXPECT_EQ(b, 2); + EXPECT_EQ(c, 3); +} + +TEST(RevRepositoryTest, NoDiffWithStampedBehavior) { + RevRepository repo1; + RevRepository repo2; + std::vector vars1(10); + std::vector vars2(10); + std::vector stamps2(10); + std::mt19937 random(12345); + for (int num_level_changes = 0; num_level_changes < 1000; + ++num_level_changes) { + const int level = absl::Uniform(random, 0, 100); + repo1.SetLevel(level); + repo2.SetLevel(level); + for (int i = 0; i < vars1.size(); ++i) { + ASSERT_EQ(vars1[i], vars2[i]); + repo1.SaveState(&vars1[i]); + } + for (int num_random_updates = 0; num_random_updates < 100; + ++num_random_updates) { + const int i = absl::Uniform(random, 0, vars1.size()); + const int value = absl::Uniform(random, 0, 1000); + vars1[i] = value; + repo2.SaveStateWithStamp(&vars2[i], &stamps2[i]); + vars2[i] = value; + } + } +} + +DEFINE_STRONG_INDEX_TYPE(TestType); + +TEST(RevVectorTest, BasicBehavior) { + RevVector v; + v.Grow(100); + + EXPECT_EQ(v[TestType(0)], 0); + EXPECT_EQ(v[TestType(10)], 0); + + v.SetLevel(3); + v.MutableRef(TestType(10)) = 4; + EXPECT_EQ(v[TestType(10)], 4); + + v.SetLevel(6); + v.MutableRef(TestType(10)) = 6; + EXPECT_EQ(v[TestType(10)], 6); + + v.SetLevel(7); + EXPECT_EQ(v[TestType(10)], 6); + + v.SetLevel(5); + EXPECT_EQ(v[TestType(10)], 4); + + v.SetLevel(0); + EXPECT_EQ(v[TestType(10)], 0); +} + +TEST(RevMapTest, LikeMapWhenNoBacktracking) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(5, m.at(4)); + m.Set(4, 6); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(6, m.at(4)); + m.EraseOrDie(4); + EXPECT_FALSE(m.contains(4)); +} + +TEST(RevMapDeathTest, EraseOrDie) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + m.EraseOrDie(4); + EXPECT_DEATH(m.EraseOrDie(4), "key not present: '4'."); +} + +TEST(RevMapTest, InsertDeleteInsertAndBacktrack) { + RevMap> m; + EXPECT_FALSE(m.contains(4)); + + m.SetLevel(1); + EXPECT_FALSE(m.contains(4)); + m.Set(4, 5); + EXPECT_TRUE(m.contains(4)); + + m.SetLevel(4); + EXPECT_TRUE(m.contains(4)); + m.EraseOrDie(4); + EXPECT_FALSE(m.contains(4)); + + // The level do not need to be contiguous, but the implementation assumes + // they are mostly dense from an efficienty point of view. + m.SetLevel(10); + EXPECT_FALSE(m.contains(4)); + m.Set(4, 6); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(6, m.at(4)); + m.Set(4, 7); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(7, m.at(4)); + + // Backtrack + m.SetLevel(4); + EXPECT_FALSE(m.contains(4)); + + m.SetLevel(1); + EXPECT_TRUE(m.contains(4)); + EXPECT_EQ(5, m.at(4)); + + m.SetLevel(0); + EXPECT_FALSE(m.contains(4)); + + // Note that the higher level info is lost: + m.SetLevel(10); + EXPECT_FALSE(m.contains(4)); +} + +TEST(RevMapTest, ManySetsOperations) { + std::mt19937 random(12345); + const int kRange = 10000; + + RevMap> m; + absl::flat_hash_map reference; + + m.SetLevel(1); + for (int i = 0; i < 10000; ++i) { + const int key = absl::Uniform(random, 0, kRange); + const int value = absl::Uniform(random, 0, kRange); + m.Set(key, value); + reference[key] = value; + } + EXPECT_EQ(m.size(), reference.size()); + + // More insert at level 2. + m.SetLevel(2); + for (int i = 0; i < 10000; ++i) { + m.Set(absl::Uniform(random, 0, kRange), absl::Uniform(random, 0, kRange)); + } + EXPECT_GT(m.size(), reference.size()); + + // Backtrack to level 1, m and reference should be the same. + m.SetLevel(1); + EXPECT_EQ(m.size(), reference.size()); + for (const auto entry : reference) { + EXPECT_EQ(entry.second, m.at(entry.first)); + } + + m.SetLevel(0); + EXPECT_TRUE(m.empty()); +} + +TEST(RevGrowingMultiMapTest, BasicTest) { + RevGrowingMultiMap map; + map.Add(0, 1); + map.Add(0, 2); + map.SetLevel(1); + map.Add(0, 2); + map.Add(1, 3); + EXPECT_EQ(map.Values(0), std::vector({1, 2, 2})); + EXPECT_EQ(map.Values(1), std::vector({3})); + map.SetLevel(0); + EXPECT_EQ(map.Values(0), std::vector({1, 2})); + EXPECT_EQ(map.Values(1), std::vector({})); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/running_stat_test.cc b/ortools/util/running_stat_test.cc new file mode 100644 index 00000000000..f00b60459cc --- /dev/null +++ b/ortools/util/running_stat_test.cc @@ -0,0 +1,121 @@ +// 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/util/running_stat.h" + +#include + +#include "gtest/gtest.h" + +namespace { + +TEST(MovingAverageTest, InitialValues) { + operations_research::RunningAverage running_average(10); + EXPECT_EQ(0.0, running_average.GlobalAverage()); + EXPECT_EQ(0.0, running_average.WindowAverage()); + EXPECT_FALSE(running_average.IsWindowFull()); +} + +TEST(MovingAverageTest, WindowOfSizeOne) { + operations_research::RunningAverage running_average(1); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + EXPECT_EQ(i, running_average.WindowAverage()); + EXPECT_EQ(static_cast(i) / 2.0, running_average.GlobalAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } +} + +TEST(MovingAverageTest, WindowOfSizeTwo) { + operations_research::RunningAverage running_average(2); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + if (i == 0) { + EXPECT_FALSE(running_average.IsWindowFull()); + } else { + EXPECT_EQ(static_cast(2 * i - 1) / 2.0, + running_average.WindowAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } + } +} + +TEST(MovingAverageTest, ClearWindow) { + operations_research::RunningAverage running_average(50); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + } + EXPECT_TRUE(running_average.IsWindowFull()); + running_average.ClearWindow(); + EXPECT_FALSE(running_average.IsWindowFull()); + EXPECT_EQ(0.0, running_average.WindowAverage()); +} + +TEST(MovingAverageTest, Reset) { + operations_research::RunningAverage running_average(50); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + } + EXPECT_TRUE(running_average.IsWindowFull()); + running_average.Reset(1); + EXPECT_EQ(0.0, running_average.GlobalAverage()); + EXPECT_EQ(0.0, running_average.WindowAverage()); + EXPECT_FALSE(running_average.IsWindowFull()); + for (int i = 0; i < 1000; ++i) { + running_average.Add(i); + EXPECT_EQ(i, running_average.WindowAverage()); + EXPECT_EQ(static_cast(i) / 2.0, running_average.GlobalAverage()); + EXPECT_TRUE(running_average.IsWindowFull()); + } +} + +TEST(RunningMaxTest, WindowOfSize1) { + operations_research::RunningMax<> running_max(1); + for (int i = 0; i < 1000; ++i) { + const double value = i % 15; + running_max.Add(value); + EXPECT_EQ(value, running_max.GetCurrentMax()); + } +} + +TEST(RunningMaxTest, SmallSequence) { + operations_research::RunningMax<> running_max(2); + running_max.Add(1); + EXPECT_EQ(1, running_max.GetCurrentMax()); + running_max.Add(10); + EXPECT_EQ(10, running_max.GetCurrentMax()); + running_max.Add(9); + EXPECT_EQ(10, running_max.GetCurrentMax()); + running_max.Add(8); + EXPECT_EQ(9, running_max.GetCurrentMax()); + running_max.Add(11); + EXPECT_EQ(11, running_max.GetCurrentMax()); +} + +TEST(RunningMaxTest, IncreasingSequence) { + operations_research::RunningMax<> running_max(10); + for (int i = 0; i < 1000; ++i) { + running_max.Add(i); + EXPECT_EQ(i, running_max.GetCurrentMax()); + } +} + +TEST(RunningMaxTest, DecreasingSequence) { + operations_research::RunningMax<> running_max(10); + for (int i = 1000; i >= 0; --i) { + running_max.Add(i); + EXPECT_EQ(std::min(1000, i + 9), running_max.GetCurrentMax()); + } +} + +} // namespace diff --git a/ortools/util/saturated_arithmetic_benchmark.cc b/ortools/util/saturated_arithmetic_benchmark.cc new file mode 100644 index 00000000000..527916daf86 --- /dev/null +++ b/ortools/util/saturated_arithmetic_benchmark.cc @@ -0,0 +1,127 @@ +// 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 +#include +#include +#include + +#include "absl/log/log.h" +#include "benchmark/benchmark.h" +#include "ortools/base/types.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { +namespace { + +std::vector GenerateInt64ValuesForTesting() { + std::vector v; + // Generate a bunch of special values... + for (int64_t base : + {int64_t{0}, kint64min, kint64max, static_cast(kint32max), + static_cast(kint32min)}) { + // ...scaled by various factor... + for (int64_t scaled : {base, base / 2, base / 3 * 2}) { + // ...with small offsets added. + for (int64_t offset : {-1000, -2, -1, 0, 1, 2, 1000}) { + // Avoid generating an overflow here. Do not want saturated as we + // want corner-case numbers, not saturated arithmetic. + v.push_back(TwosComplementAddition(scaled, offset)); + } + } + } + return v; +} + +std::vector NonOverflowCases() { + std::vector v; + for (int64_t i = -50; i < 50; ++i) { + v.push_back(i); + } + return v; +} + +// A renaming of GenerateInt64ValuesForTesting() to make the display of +// the micro-benchmarks look nicer. +inline std::vector CornerCases() { + return GenerateInt64ValuesForTesting(); +} + +// In the benchmark, we can use either the "special" testing values, or +// some "safe" values to measure peak performance. +#define BENCH_INT64_OPERATOR(Operator, GenerationFunction) \ + void BM_##Operator##_##GenerationFunction(benchmark::State& state) { \ + std::vector values1 = GenerationFunction(); \ + std::vector values2 = values1; \ + std::mt19937 random(12345); \ + std::shuffle(values2.begin(), values2.end(), random); \ + int64_t ret = 0; \ + for (auto _ : state) { \ + for (const int64_t a : values1) { \ + for (const int64_t b : values2) { \ + ret |= Operator(a, b); \ + } \ + } \ + } \ + state.SetItemsProcessed(state.max_iterations * values1.size() * \ + values2.size()); \ + VLOG(1) << "Result: " << ret; \ + } \ + BENCHMARK(BM_##Operator##_##GenerationFunction) + +// To be used only for speed benchmarking, on non-overflowing test cases. +inline int64_t SimpleMultiplication(int64_t x, int64_t y) { return x * y; } + +BENCH_INT64_OPERATOR(CapAdd, CornerCases); +BENCH_INT64_OPERATOR(CapSub, CornerCases); +BENCH_INT64_OPERATOR(CapProd, CornerCases); +#if defined(__GNUC__) && defined(__x86_64__) +BENCH_INT64_OPERATOR(CapAddAsm, CornerCases); +BENCH_INT64_OPERATOR(CapSubAsm, CornerCases); +BENCH_INT64_OPERATOR(CapProdAsm, CornerCases); +#endif +#if defined(__clang__) +BENCH_INT64_OPERATOR(CapAddBuiltIn, CornerCases); +BENCH_INT64_OPERATOR(CapSubBuiltIn, CornerCases); +BENCH_INT64_OPERATOR(CapProdBuiltIn, CornerCases); +#endif + +BENCH_INT64_OPERATOR(CapAddGeneric, CornerCases); +BENCH_INT64_OPERATOR(CapSubGeneric, CornerCases); +BENCH_INT64_OPERATOR(CapProdGeneric, CornerCases); + +BENCH_INT64_OPERATOR(CapAdd, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSub, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProd, NonOverflowCases); +#if defined(__GNUC__) && defined(__x86_64__) +BENCH_INT64_OPERATOR(CapAddAsm, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubAsm, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdAsm, NonOverflowCases); +#endif +#if defined(__clang__) +BENCH_INT64_OPERATOR(CapAddBuiltIn, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubBuiltIn, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdBuiltIn, NonOverflowCases); +#endif + +BENCH_INT64_OPERATOR(CapAddGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(CapSubGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(CapProdGeneric, NonOverflowCases); +BENCH_INT64_OPERATOR(TwosComplementAddition, NonOverflowCases); +BENCH_INT64_OPERATOR(TwosComplementSubtraction, NonOverflowCases); +BENCH_INT64_OPERATOR(SimpleMultiplication, NonOverflowCases); + +// TODO(user): do the same for CapOpp. + +} // namespace +} // namespace operations_research diff --git a/ortools/util/saturated_arithmetic_test.cc b/ortools/util/saturated_arithmetic_test.cc new file mode 100644 index 00000000000..655bcb70c74 --- /dev/null +++ b/ortools/util/saturated_arithmetic_test.cc @@ -0,0 +1,231 @@ +// 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/util/saturated_arithmetic.h" + +#include +#include +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/numeric/int128.h" +#include "absl/random/random.h" +#include "gtest/gtest.h" +#include "ortools/base/types.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace { + +DEFINE_STRONG_INT64_TYPE(TestType); + +void TestSafeAdd64(int64_t a, int64_t b, bool error_expected) { + TestType result_b(b); + TestType result_a(a); + EXPECT_EQ(SafeAddInto(TestType(a), &result_b), !error_expected) + << a << " + " << b; + EXPECT_EQ(SafeAddInto(TestType(b), &result_a), !error_expected) + << a << " + " << b; + if (!error_expected) { + EXPECT_EQ(result_a.value(), a + b) << a << " + " << b; + EXPECT_EQ(result_b.value(), a + b) << a << " + " << b; + } +} + +TEST(SafeAddTest, BasicCases) { + TestSafeAdd64(5, 42, /*error_expected*/ false); + TestSafeAdd64(kint64min / 2, kint64min / 2, /*error_expected=*/false); + TestSafeAdd64(kint64max / 2, kint64max / 2, /*error_expected=*/false); + TestSafeAdd64(kint64min / 2, kint64min / 2 - 2, + /*error_expected=*/true); + TestSafeAdd64(kint64max / 2, kint64max / 2 + 2, + /*error_expected=*/true); + TestSafeAdd64(kint64min / 2, kint64min / 2 - 1, + /*error_expected=*/true); + TestSafeAdd64(kint64max / 2, kint64max / 2 + 1, + /*error_expected=*/false); + TestSafeAdd64(kint64min, -1, /*error_expected=*/true); + TestSafeAdd64(kint64max, +1, /*error_expected=*/true); + TestSafeAdd64(kint64min, +1, /*error_expected=*/false); + TestSafeAdd64(kint64max, -1, /*error_expected=*/false); +} + +std::vector GenerateInt64ValuesForTesting() { + std::vector v; + // Generate a bunch of special values... + for (int64_t base : + {int64_t{0}, kint64min, kint64max, static_cast(kint32max), + static_cast(kint32min)}) { + // ...scaled by various factor... + for (int64_t scaled : {base, base / 2, base / 3 * 2}) { + // ...with small offsets added. + for (int64_t offset : {-1000, -2, -1, 0, 1, 2, 1000}) { + // Avoid generating an overflow here. Do not want saturated as we + // want corner-case numbers, not saturated arithmetic. + v.push_back(TwosComplementAddition(scaled, offset)); + } + } + } + return v; +} + +class SaturatedArithmeticTest : public ::testing::Test { + protected: + // Reference (safe, but slow) versions of the functions whose behavior + // we are testing. We'd like to use int128, but base/int128.h only provides + // uint128, so we use bit tricks, and define our own conversions + // uint128 <-> int64_t (see the "private" section). + int64_t ReferenceCapAdd(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) + Int64To128(b)); + } + int64_t ReferenceCapSub(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) - Int64To128(b)); + } + int64_t ReferenceCapOpp(int64_t v) { + return Int128ToCapped64(-Int64To128(v)); + } + int64_t ReferenceCapProd(int64_t a, int64_t b) { + return Int128ToCapped64(Int64To128(a) * Int64To128(b)); + } + + void SetUp() override { + int64_values_ = GenerateInt64ValuesForTesting(); + // Now, add a few random values. + const int kNumRandomValues = 1000; + std::mt19937 random(12345); + for (int i = 0; i < kNumRandomValues; ++i) { + int64_values_.push_back( + static_cast(absl::Uniform(random))); + } + } + + std::vector int64_values_; + + private: + bool IsNegative(absl::uint128 x) { + return static_cast(absl::Uint128High64(x)) < 0; + } + + absl::uint128 Int64To128(int64_t x) { + return absl::MakeUint128(static_cast(x < 0 ? -1 : 0), + static_cast(x)); + } + + int64_t Int128ToCapped64(absl::uint128 x) { + const int64_t low = static_cast(absl::Uint128Low64(x)); + if (IsNegative(x)) { + if (absl::Uint128High64(x) != static_cast(-1)) return kint64min; + if (low >= 0) return kint64min; + return low; + } + if (absl::Uint128High64(x) != 0) return kint64max; + if (low < 0) return kint64max; + return low; + } + + uint64_t Uint128ToCappedU64(absl::uint128 x) { + return x > absl::uint128(kuint64max) ? kuint64max : absl::Uint128Low64(x); + } +}; + +// Don't use EXPECT to avoid gunit issues -- we test ~1M pairs of +// values, and generating that much output can freeze your test. +#define TEST_BINARY_OPERATOR(name, symbol, reference) \ + TEST_F(SaturatedArithmeticTest, name) { \ + for (int64_t a : int64_values_) { \ + for (int64_t b : int64_values_) { \ + ASSERT_EQ(reference(a, b), name(a, b)) \ + << a << " " << symbol << " " << b; \ + } \ + } \ + } + +TEST_BINARY_OPERATOR(CapAdd, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapAddGeneric, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapSub, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapSubGeneric, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapProd, "*", ReferenceCapProd); +TEST_BINARY_OPERATOR(CapProdGeneric, "*", ReferenceCapProd); + +#if defined(__GNUC__) && defined(__x86_64__) +TEST_BINARY_OPERATOR(CapAddAsm, "+", ReferenceCapAdd); +TEST_BINARY_OPERATOR(CapSubAsm, "-", ReferenceCapSub); +TEST_BINARY_OPERATOR(CapProdAsm, "*", ReferenceCapProd); +#endif + +TEST_F(SaturatedArithmeticTest, CapOpp) { + for (int64_t a : int64_values_) { + ASSERT_EQ(ReferenceCapOpp(a), CapOpp(a)) << a; + } +} + +TEST_F(SaturatedArithmeticTest, CapAbs) { + ASSERT_EQ(CapAbs(kint64min), kint64max); + ASSERT_EQ(CapAbs(kint64max), kint64max); + ASSERT_EQ(CapAbs(-100), 100); + ASSERT_EQ(CapAbs(0), 0); + ASSERT_EQ(CapAbs(200), 200); +} + +TEST_F(SaturatedArithmeticTest, CapAddTo) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = a; + CapAddTo(b, &result); + ASSERT_EQ(result, CapAdd(a, b)) << a << " + " << b; + } + } +} + +TEST_F(SaturatedArithmeticTest, AddIntoOverflow) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = b; + if (AddIntoOverflow(a, &result)) { + EXPECT_TRUE(AddOverflows(a, b)); + } else { + EXPECT_FALSE(AddOverflows(a, b)); + EXPECT_EQ(result, a + b); + } + } + } +} + +TEST_F(SaturatedArithmeticTest, CapSubFrom) { + for (int64_t a : int64_values_) { + for (int64_t b : int64_values_) { + int64_t result = a; + CapSubFrom(/*amount=*/b, /*target=*/&result); + ASSERT_EQ(result, CapSub(a, b)) << a << " + " << b; + } + } +} + +TEST(CapOrFloatAdd, VariousTypes) { + constexpr double kInf = std::numeric_limits::infinity(); + EXPECT_EQ(CapOrFloatAdd(0.5, 0.25), 0.75); + EXPECT_EQ(CapOrFloatAdd(kInf, 0.25), kInf); + EXPECT_EQ(CapOrFloatAdd(int64_t{1}, kint64max), kint64max); + EXPECT_EQ(CapOrFloatAdd(4, kint32max - 2), kint32max); + + // A corner case: float isn't accurate enough to represent 1+1e-10, which we + // use to verify that the return type is the same as the argument type. + EXPECT_EQ(static_cast(CapOrFloatAdd(float{1}, float{1e-10})), 1.0); +} + +// TODO(user): do the same for CapOpp. + +} // namespace +} // namespace operations_research diff --git a/ortools/util/solve_interrupter_test.cc b/ortools/util/solve_interrupter_test.cc new file mode 100644 index 00000000000..c1599291b9f --- /dev/null +++ b/ortools/util/solve_interrupter_test.cc @@ -0,0 +1,204 @@ +// 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/util/solve_interrupter.h" + +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" + +namespace operations_research { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +TEST(SolveInterrupterTest, NotCopyableOrAssignable) { + EXPECT_FALSE(std::is_copy_constructible::value); + EXPECT_FALSE(std::is_copy_assignable::value); +} + +TEST(SolveInterrupterTest, Untriggered) { + // Note that we use a `const SolveInterrupter` here to test that the + // "readonly" part of the interface is indeed `const`. + const SolveInterrupter interrupter; + EXPECT_FALSE(interrupter.IsInterrupted()); + + std::vector calls; + const SolveInterrupter::CallbackId id = + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + calls.push_back(interrupter.IsInterrupted()); + }); + + // Test that AddInterruptionCallback() has not called the callback. + EXPECT_THAT(calls, IsEmpty()); + + interrupter.RemoveInterruptionCallback(id); +} + +TEST(SolveInterrupterTest, Triggered) { + SolveInterrupter interrupter; + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + + std::vector calls; + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + calls.push_back(interrupter.IsInterrupted()); + }); + + // Test that AddInterruptionCallback() has called the callback once and that + // IsInterrupted() was true at that time. + EXPECT_THAT(calls, ElementsAre(true)); +} + +TEST(SolveInterrupterTest, Triggering) { + SolveInterrupter interrupter; + + std::vector calls; + interrupter.AddInterruptionCallback([&]() { + calls.push_back(absl::StrCat("callback1: ", interrupter.IsInterrupted())); + }); + interrupter.AddInterruptionCallback([&]() { + calls.push_back(absl::StrCat("callback2: ", interrupter.IsInterrupted())); + }); + + EXPECT_THAT(calls, IsEmpty()); + + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(calls, ElementsAre("callback1: 1", "callback2: 1")); + + // Testing that calling Interrupt() a second time does not call the callbacks + // again. + calls.clear(); + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(calls, IsEmpty()); +} + +TEST(SolveInterrupterTest, Unregistering) { + SolveInterrupter interrupter; + + std::vector callback1_calls; + const SolveInterrupter::CallbackId callback1 = + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + callback1_calls.push_back(interrupter.IsInterrupted()); + }); + std::vector callback2_calls; + interrupter.AddInterruptionCallback([&]() { + // Test that we can call IsInterrupted() from the callback on top of + // registering all calls. + callback2_calls.push_back(interrupter.IsInterrupted()); + }); + + EXPECT_THAT(callback1_calls, IsEmpty()); + EXPECT_THAT(callback2_calls, IsEmpty()); + + interrupter.RemoveInterruptionCallback(callback1); + interrupter.Interrupt(); + + EXPECT_TRUE(interrupter.IsInterrupted()); + EXPECT_THAT(callback1_calls, IsEmpty()); + EXPECT_THAT(callback2_calls, ElementsAre(true)); +} + +TEST(SolveInterrupterDeathTest, UnregisteringTwice) { + SolveInterrupter interrupter; + const SolveInterrupter::CallbackId callback = + interrupter.AddInterruptionCallback([]() {}); + + interrupter.RemoveInterruptionCallback(callback); + EXPECT_DEATH(interrupter.RemoveInterruptionCallback(callback), + "unregistered callback id"); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackNotCalled) { + // Note that we use a `const` variable here to test that we can use + // ScopedSolveInterrupterCallbackTest with a pointer to a `const + // SolveInterrupter`. + const SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackCalled) { + SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + interrupter.Interrupt(); + } + EXPECT_EQ(calls, 1); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackRemoved) { + SolveInterrupter interrupter; + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + } + interrupter.Interrupt(); + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, CallbackRemovedBeforeDestruction) { + SolveInterrupter interrupter; + int calls = 0; + { + ScopedSolveInterrupterCallback scoped_callback(&interrupter, + [&]() { ++calls; }); + scoped_callback.RemoveCallbackIfNecessary(); + interrupter.Interrupt(); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, NullInterrupter) { + int calls = 0; + { + const ScopedSolveInterrupterCallback scoped_callback(nullptr, + [&]() { ++calls; }); + } + EXPECT_EQ(calls, 0); +} + +TEST(ScopedSolveInterrupterCallbackTest, RemoveCallbackNullInterrupter) { + int calls = 0; + { + ScopedSolveInterrupterCallback scoped_callback(nullptr, [&]() { ++calls; }); + scoped_callback.RemoveCallbackIfNecessary(); + } + EXPECT_EQ(calls, 0); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/status.proto b/ortools/util/status.proto new file mode 100644 index 00000000000..d4d063c05e9 --- /dev/null +++ b/ortools/util/status.proto @@ -0,0 +1,29 @@ +// 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. + +syntax = "proto3"; + +package operations_research; + +option java_package = "com.google.operationsresearch"; +option java_multiple_files = true; +option java_outer_classname = "StatusProtobuf"; + +// The streamed version of absl::Status. +message StatusProto { + // The status code, one of the absl::StatusCode. + int32 code = 1; + + // The status message. + string message = 2; +} diff --git a/ortools/util/status_macros_test.cc b/ortools/util/status_macros_test.cc new file mode 100644 index 00000000000..58e3b80e62c --- /dev/null +++ b/ortools/util/status_macros_test.cc @@ -0,0 +1,61 @@ +// 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/util/status_macros.h" + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/status_macros.h" + +namespace operations_research { +namespace { + +using ::testing::HasSubstr; +using ::testing::Pair; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +TEST(OrAssignOrReturn3Test, CostlyExpression) { + // We use a function in the test to validate that OR_ASSIGN_OR_RETURN3 does + // not evaluate the third parameters unless the status is failing. + int costly_function_calls = 0; + const auto costly_function = [&]() { + ++costly_function_calls; + return "costly result"; + }; + + const auto macro_invoker = + [&](const absl::StatusOr> input) + -> absl::StatusOr> { + // Here we also test that we can use destructured assignment, even if we + // want a pair in the end. + OR_ASSIGN_OR_RETURN3((const auto [x, y]), input, _ << costly_function()); + return std::make_pair(x, y); + }; + + EXPECT_THAT(macro_invoker(std::make_pair(3, -1.5)), + IsOkAndHolds(Pair(3, -1.5))); + EXPECT_EQ(costly_function_calls, 0); + + EXPECT_THAT(macro_invoker(absl::InternalError("oops")), + StatusIs(absl::StatusCode::kInternal, + AllOf(HasSubstr("oops"), HasSubstr("costly result")))); + EXPECT_EQ(costly_function_calls, 1); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/status_streaming.cc b/ortools/util/status_streaming.cc new file mode 100644 index 00000000000..1995ff0b757 --- /dev/null +++ b/ortools/util/status_streaming.cc @@ -0,0 +1,45 @@ +// 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/util/status_streaming.h" + +#include + +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/cord.h" +#include "absl/strings/string_view.h" +#include "ortools/base/source_location.h" + +namespace operations_research { + +StatusProto StreamStatus(const absl::Status& status, + const absl::SourceLocation warning_loc) { + StatusProto ret; + ret.set_code(static_cast(status.code())); + ret.set_message(status.message()); + status.ForEachPayload( + [&](const absl::string_view type_url, const absl::Cord&) { + LOG(WARNING).AtLocation(warning_loc.file_name(), warning_loc.line()) + << "Ignored payload: " << type_url; + }); + return ret; +} + +absl::Status UnstreamStatus(const StatusProto& status_proto, + absl::SourceLocation loc) { + return absl::Status(static_cast(status_proto.code()), + status_proto.message()); +} + +} // namespace operations_research diff --git a/ortools/util/status_streaming.h b/ortools/util/status_streaming.h new file mode 100644 index 00000000000..58afff77563 --- /dev/null +++ b/ortools/util/status_streaming.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef ORTOOLS_UTIL_STATUS_STREAMING_H_ +#define ORTOOLS_UTIL_STATUS_STREAMING_H_ + +#include "absl/status/status.h" +#include "ortools/base/source_location.h" +#include "ortools/util/status.pb.h" + +namespace operations_research { + +// Streams the input Status in a proto. +// +// Note that payloads are not supported; a LOG(WARNING) will be printed if there +// are payloads in the input Status, using `warning_loc` as location. +StatusProto StreamStatus( + const absl::Status& status, + absl::SourceLocation warning_loc = absl::SourceLocation::current()); + +// Unstreams the input proto in the corresponding Status. +// +// The `loc` parameter is used as the status' location. +absl::Status UnstreamStatus( + const StatusProto& status_proto, + absl::SourceLocation loc = absl::SourceLocation::current()); + +} // namespace operations_research + +#endif // ORTOOLS_UTIL_STATUS_STREAMING_H_ diff --git a/ortools/util/status_streaming_test.cc b/ortools/util/status_streaming_test.cc new file mode 100644 index 00000000000..86b58a37639 --- /dev/null +++ b/ortools/util/status_streaming_test.cc @@ -0,0 +1,89 @@ +// 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/util/status_streaming.h" + +#include + +#include "absl/log/scoped_mock_log.h" +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/base/source_location.h" + +namespace operations_research { +namespace { + +using ::testing::AnyNumber; +using ::testing::ElementsAre; +using ::testing::EqualsProto; +using ::testing::HasSubstr; +using ::testing::Property; +using ::testing::status::StatusIs; + +TEST(StreamStatusTest, Ok) { + EXPECT_THAT(StreamStatus(absl::OkStatus()), EqualsProto(StatusProto{})); +} + +TEST(StreamStatusTest, Failure) { + StatusProto expected; + expected.set_code(static_cast(absl::StatusCode::kUnimplemented)); + expected.set_message("some message"); + EXPECT_THAT(StreamStatus(absl::UnimplementedError("some message")), + EqualsProto(expected)); +} + +TEST(StreamStatusTest, FailureWithPayload) { + absl::Status status = absl::UnimplementedError("some message"); + status.SetPayload("some_payload_url", {}); + + StatusProto expected; + expected.set_code(static_cast(absl::StatusCode::kUnimplemented)); + expected.set_message("some message"); + + absl::ScopedMockLog log; + EXPECT_CALL(log, Log).Times(AnyNumber()); + EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, ::testing::_, + HasSubstr("some_payload_url"))) + .Times(1); + + log.StartCapturingLogs(); + EXPECT_THAT(StreamStatus(status), EqualsProto(expected)); +} + +TEST(UnstreamStatusTest, Ok) { EXPECT_OK(UnstreamStatus(StatusProto{})); } + +TEST(UnstreamStatusTest, FailureWithDefaultLocation) { + StatusProto proto; + proto.set_code(static_cast(absl::StatusCode::kUnimplemented)); + proto.set_message("some message"); + + const absl::Status unstreamed_status = UnstreamStatus(proto); + EXPECT_THAT(unstreamed_status, + StatusIs(absl::StatusCode::kUnimplemented, "some message")); +} + +TEST(UnstreamStatusTest, FailureWithCustomLocation) { + StatusProto proto; + proto.set_code(static_cast(absl::StatusCode::kUnimplemented)); + proto.set_message("some message"); + + const absl::SourceLocation location = absl::SourceLocation::current(); + const absl::Status unstreamed_status = UnstreamStatus(proto, location); + EXPECT_THAT(unstreamed_status, + StatusIs(absl::StatusCode::kUnimplemented, "some message")); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/testing_utils.h b/ortools/util/testing_utils.h index 370c10ff1f0..6b3305076ce 100644 --- a/ortools/util/testing_utils.h +++ b/ortools/util/testing_utils.h @@ -17,12 +17,13 @@ namespace operations_research { inline constexpr bool kAsanEnabled = false; +inline constexpr bool kHwAsanEnabled = false; inline constexpr bool kMsanEnabled = false; inline constexpr bool kTsanEnabled = false; inline bool ProbablyRunningInsideUnitTest() { return false; } inline constexpr bool kAnyXsanEnabled = - kAsanEnabled || kMsanEnabled || kTsanEnabled; + kAsanEnabled || kMsanEnabled || kTsanEnabled || kHwAsanEnabled; } // namespace operations_research diff --git a/ortools/util/time_limit_benchmark.cc b/ortools/util/time_limit_benchmark.cc new file mode 100644 index 00000000000..b75c50d56b0 --- /dev/null +++ b/ortools/util/time_limit_benchmark.cc @@ -0,0 +1,104 @@ +// 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 + +#include "absl/log/check.h" +#include "absl/strings/str_cat.h" +#include "benchmark/benchmark.h" +#include "ortools/util/time_limit.h" + +namespace operations_research { +namespace { + +const double kInfinity = std::numeric_limits::infinity(); + +template +static void BM_TimeLimitReached(benchmark::State& state) { + for (auto _ : state) { + TimeLimit time_limit(1.0); + double sum = 0.0; + for (int i = 0; i < 10000; ++i) { + if (with_time_limit) { + CHECK(!time_limit.LimitReached()); + } + // Something that takes some time. + for (int j = 0; j < 50; ++j) { + sum += i * j; + } + } + CHECK(!time_limit.LimitReached()); + CHECK_GE(sum, 0.0); + } + state.SetLabel(absl::StrCat(with_time_limit ? "With" : "Without", + " TimeLimitReached()")); +} + +// This just gives an idea of how slow it is to call LimitReached(). +// blaze run -c opt --linkopt=-static --dynamic_mode=off +// ortools/util:time_limit_test -- --benchmarks=all +// +// Here are perflab benchmark results: +// Run on lpl35 (24 X 2800 MHz CPUs); 2014/05/21-04:12:59 +// CPU: Intel Westmere with HyperThreading (12 cores) dL1:32KB dL2:256KB +// Benchmark Time(ns) CPU(ns) Iterations +// ----------------------------------------------------------- +// BM_TimeLimitReached 716538 713331 981 Without +// BM_TimeLimitReached 742439 739093 947 With TimeLimit +BENCHMARK_TEMPLATE(BM_TimeLimitReached, /*with_time_limit=*/false); +BENCHMARK_TEMPLATE(BM_TimeLimitReached, /*with_time_limit=*/true); + +// This is to verify that the messages do not have any effect on performance +// of the optimized code (that the compiler in opt mode removes the messages). +// blaze run -c opt --linkopt=static --dynamic_mode=off +// //ortools/util:time_limit_test -- --benchmarks=all +// +// Here are perflab benchmark results: +// Run on lpab10 (32 X 2600 MHz CPUs); 2015/07/27-02:59:39 +// CPU: Intel Sandybridge with HyperThreading (16 cores) dL1:32KB dL2:256KB +// Benchmark Time(ns) CPU(ns) Iterations +// ------------------------------------------------------------------- +// BM_AdvanceDeterministicTime 514067 513431 1000 Without +// BM_AdvanceDeterministicTime 440777 440644 1596 With +// +// Note that the difference between the two benchmarks is most likely because of +// memory cache behavior. The same difference (but with opposite values) is +// observed when the order of the two benchmarks is reversed. +template +static void BM_AdvanceDeterministicTime(benchmark::State& state) { + for (auto _ : state) { + TimeLimit time_limit(kInfinity, 1000.0); + double sum = 0.0; + while (!time_limit.LimitReached()) { + // Something that takes time. + for (int i = 0; i < 500; ++i) { + sum += i * i; + } + if (with_messages) { + time_limit.AdvanceDeterministicTime(1.0, "AdvanceTime"); + } else { + time_limit.AdvanceDeterministicTime(1.0); + } + } + CHECK_GE(sum, 0.0); + } + state.SetLabel(absl::StrCat("AdvanceDeterministicTime", + with_messages ? "With" : "Without", + "NamedCounters")); +} + +BENCHMARK_TEMPLATE(BM_AdvanceDeterministicTime, /*with_messages=*/false); +BENCHMARK_TEMPLATE(BM_AdvanceDeterministicTime, /*with_messages=*/true); + +} // namespace +} // namespace operations_research diff --git a/ortools/util/time_limit_test.cc b/ortools/util/time_limit_test.cc new file mode 100644 index 00000000000..949cc2e31f8 --- /dev/null +++ b/ortools/util/time_limit_test.cc @@ -0,0 +1,423 @@ +// 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/util/time_limit.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/macros.h" +#include "absl/flags/flag.h" +#include "absl/log/log_streamer.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/base/threadpool.h" +#include "ortools/util/testing_utils.h" + +namespace operations_research { +namespace { + +using ::testing::MatchesRegex; +using ::testing::UnorderedElementsAre; + +const double kInfinity = std::numeric_limits::infinity(); +constexpr int kTimeMultiplier = + (DEBUG_MODE ? 5 : 1) * (kAnyXsanEnabled ? 2 : 1); + +TEST(TimeLimitTest, NoLimit) { + TimeLimit time_limit(kInfinity); + for (int i = 0; i < 100; ++i) { + EXPECT_FALSE(time_limit.LimitReached()); + } + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, NoLimitSecondaryBoolean) { + TimeLimit time_limit(kInfinity); + std::atomic stop = false; + time_limit.RegisterSecondaryExternalBooleanAsLimit(&stop); + EXPECT_FALSE(time_limit.LimitReached()); + stop.store(true); + EXPECT_TRUE(time_limit.LimitReached()); +} + +TEST(TimeLimitTest, ZeroLimit) { + TimeLimit time_limit(0.0, 0.0); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, NegativeLimit) { + TimeLimit time_limit(-100); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, NegativeInfiniteLimit) { + TimeLimit time_limit(-kInfinity); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, SmallestLimit) { + TimeLimit time_limit(1e-6); + // We are defensive and return that the limit is reached if less than 0.1ms is + // left. + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + // Even though the limit is reached, some time is left because we are + // defensive in the limit enforcement. + EXPECT_GT(1e-7, time_limit.GetTimeLeft()); +} + +TEST(TimeLimitTest, SmallLimit) { + // FLAKINESS: This test is hard to de-flake, so we simply try a few times. + constexpr int kLastAttempt = 5; + for (int attempt = 0;; ++attempt) { + TimeLimit time_limit(1e-3 * kTimeMultiplier); + if (time_limit.LimitReached()) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + double sum = 0.0; + for (int i = 0; i < 100; ++i) { + if (time_limit.LimitReached()) break; + sum += i * i; + } + if (time_limit.LimitReached() || time_limit.GetTimeLeft() < 0) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + // The rest of the test below isn't expected to be flaky. + while (true) { + if (time_limit.LimitReached()) break; + sum += 123456789.0; + } + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_GE(sum, 0.0); + break; + } +} + +TEST(TimeLimitTest, TimeSeemsRight) { + for (bool usertime_flag : {false, true}) { + SCOPED_TRACE(absl::StrFormat("usertime_flag: %v", usertime_flag)); + // UserTimer is not implemented yet + if (usertime_flag) continue; + + const absl::Duration kSleepInterval = + absl::Milliseconds(10) * kTimeMultiplier; + constexpr int kSafetyBufferIterations = 5; + constexpr int kNumIterations = 10; + const absl::Duration kTimeLimit = kSleepInterval * kNumIterations; + // FLAKINESS: This test is hard to de-flake, so we simply re-try many times. + constexpr int kLastAttempt = 40; + for (int attempt = 0;; ++attempt) { + TimeLimit time_limit(absl::ToDoubleSeconds(kTimeLimit)); + EXPECT_FALSE(time_limit.LimitReached()); + int limit_reached_at = -1; + for (int i = 0; i < kNumIterations - kSafetyBufferIterations; ++i) { + absl::SleepFor(kSleepInterval); + if (time_limit.LimitReached()) { + limit_reached_at = i; + break; + } + } + if (limit_reached_at >= 0) { + ASSERT_LT(attempt, kLastAttempt) + << "Limit reached too early -- even after " << kLastAttempt + 1 + << " attempt. i=" << limit_reached_at; + continue; // Start a new attempt. + } + + for (int i = 0; i < kSafetyBufferIterations; ++i) { + absl::SleepFor(kSleepInterval); + } + if (absl::GetFlag(FLAGS_time_limit_use_usertime)) { + // Sleep doesn't count towards the usertime! + if (time_limit.LimitReached() || + absl::Seconds(time_limit.GetTimeLeft()) < + kTimeLimit - absl::Milliseconds(50) * kTimeMultiplier) { + ASSERT_LT(attempt, kLastAttempt); + continue; + } + } else { + // This part isn't flaky at all. + ASSERT_TRUE(time_limit.LimitReached()); + } + break; // We've succeeded! No need to re-attempt. + } + } +} + +TEST(TimeLimitTest, NegativeDeterministicLimit) { + TimeLimit time_limit(kInfinity, -100); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + + TimeLimit both_time_limit(-100, -100); + EXPECT_TRUE(both_time_limit.LimitReached()); + EXPECT_TRUE(both_time_limit.LimitReached()); + EXPECT_EQ(0.0, both_time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, both_time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, DeterministicTimeOnly) { + const double kDeterministicLimit = 10.0; + TimeLimit time_limit(kInfinity, kDeterministicLimit); + // Consume all the deterministic time. + for (int i = 0; i < kDeterministicLimit; ++i) { + EXPECT_FALSE(time_limit.LimitReached()); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(kDeterministicLimit - i, time_limit.GetDeterministicTimeLeft()); + EXPECT_EQ(i, time_limit.GetElapsedDeterministicTime()); + time_limit.AdvanceDeterministicTime(1.0); + } + // The limit should be reached. + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + + // Consuming one more deterministic time. + time_limit.AdvanceDeterministicTime(1.0); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(kInfinity, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, AdvanceDeterministicTimeWithCounterName) { + static const char kCounterFoo[] = "DoingFoo"; + static const char kCounterBar[] = "DoingBar"; + static const double kDeterministicStepFoo = 1.0; + static const double kDeterministicStepBar = 2.0; + static const int kNumSteps = 10; + static const double kExpectedElapseDeterministicTime = 30.0; + TimeLimit time_limit(kInfinity, kInfinity); + for (int i = 0; i < kNumSteps; ++i) { + time_limit.AdvanceDeterministicTime(kDeterministicStepFoo, kCounterFoo); + time_limit.AdvanceDeterministicTime(kDeterministicStepBar, kCounterBar); + } + EXPECT_NEAR(kExpectedElapseDeterministicTime, + time_limit.GetElapsedDeterministicTime(), 1e-10); + const std::vector debug_string_lines = + absl::StrSplit(time_limit.DebugString(), '\n'); +#ifdef NDEBUG + EXPECT_THAT( + debug_string_lines, + UnorderedElementsAre("Time left: inf", "Deterministic time left: inf", + MatchesRegex("Elapsed time: [-0-9.e]*"), + "Elapsed deterministic time: 30")); +#else + EXPECT_THAT( + debug_string_lines, + UnorderedElementsAre("Time left: inf", "Deterministic time left: inf", + MatchesRegex("Elapsed time: [-0-9.e]*"), + "Elapsed deterministic time: 30", "DoingFoo: 10", + "DoingBar: 20")); +#endif +} + +TEST(TimeLimitTest, ElapsedTimeReachedFirst) { + const double kLimit = 0.01; // 10ms + const double kUnitTime = 0.001; + TimeLimit time_limit(kLimit, kLimit); + EXPECT_FALSE(time_limit.LimitReached()); + + // Sleeps 100ms. + absl::SleepFor(absl::Milliseconds(100)); + time_limit.AdvanceDeterministicTime(kUnitTime); + EXPECT_GE(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(kLimit - kUnitTime, time_limit.GetDeterministicTimeLeft()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_EQ(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(kLimit - kUnitTime, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, DeterniministicTimeReachedFirst) { + const double kLimit = 0.2 * kTimeMultiplier; // 200ms in opt. + TimeLimit time_limit(kLimit, kLimit); + EXPECT_FALSE(time_limit.LimitReached()); + + absl::SleepFor(absl::Milliseconds(1) * kTimeMultiplier); + time_limit.AdvanceDeterministicTime(1.0 * kTimeMultiplier); + EXPECT_LT(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); + EXPECT_TRUE(time_limit.LimitReached()); + EXPECT_LT(0.0, time_limit.GetTimeLeft()); + EXPECT_EQ(0.0, time_limit.GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, ExternalLimitReached) { + std::atomic external_boolean_as_limit(false); + TimeLimit time_limit(kInfinity, kInfinity); + time_limit.RegisterExternalBooleanAsLimit(&external_boolean_as_limit); + EXPECT_FALSE(time_limit.LimitReached()); + + absl::SleepFor(absl::Milliseconds(10)); + time_limit.AdvanceDeterministicTime(1.0); + EXPECT_FALSE(time_limit.LimitReached()); + + external_boolean_as_limit = true; + EXPECT_TRUE(time_limit.LimitReached()); + + external_boolean_as_limit = false; + EXPECT_FALSE(time_limit.LimitReached()); +} + +TEST(TimeLimitTest, Infinite) { + std::unique_ptr time_limit = TimeLimit::Infinite(); + EXPECT_EQ(kInfinity, time_limit->GetTimeLeft()); + EXPECT_EQ(kInfinity, time_limit->GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, FromDeterministicTime) { + const double kDeterministicTimeLimit = 2.0; + std::unique_ptr time_limit = + TimeLimit::FromDeterministicTime(kDeterministicTimeLimit); + EXPECT_EQ(kInfinity, time_limit->GetTimeLeft()); + EXPECT_EQ(kDeterministicTimeLimit, time_limit->GetDeterministicTimeLeft()); +} + +class TestParameters { + public: + TestParameters(double max_time_in_seconds, double max_deterministic_time) + : max_time_in_seconds_(max_time_in_seconds), + max_deterministic_time_(max_deterministic_time) {} + + double max_time_in_seconds() const { return max_time_in_seconds_; } + double max_deterministic_time() const { return max_deterministic_time_; } + + private: + double max_time_in_seconds_; + double max_deterministic_time_; +}; + +TEST(TimeLimitTest, FromParameters) { + const double kTimeLimitSeconds = 1.0; + const double kDeterministicTimeLimit = 2.0; + TestParameters parameters(kTimeLimitSeconds, kDeterministicTimeLimit); + std::unique_ptr time_limit = TimeLimit::FromParameters(parameters); + EXPECT_NEAR(kTimeLimitSeconds, time_limit->GetTimeLeft(), + 1e-3 * kTimeMultiplier); + EXPECT_EQ(kDeterministicTimeLimit, time_limit->GetDeterministicTimeLeft()); +} + +TEST(TimeLimitTest, ResetLimitFromParameters) { + TestParameters parameters(kInfinity, 1.0); + std::unique_ptr time_limit = TimeLimit::FromParameters(parameters); + EXPECT_FALSE(time_limit->LimitReached()); + time_limit->AdvanceDeterministicTime(2.0); + EXPECT_TRUE(time_limit->LimitReached()); + EXPECT_EQ(time_limit->GetElapsedDeterministicTime(), 2.0); + EXPECT_EQ(time_limit->GetDeterministicTimeLeft(), 0.0); + + time_limit->ResetLimitFromParameters(parameters); + EXPECT_FALSE(time_limit->LimitReached()); + EXPECT_EQ(time_limit->GetElapsedDeterministicTime(), 0.0); + EXPECT_EQ(time_limit->GetDeterministicTimeLeft(), 1.0); + + time_limit->AdvanceDeterministicTime(2.0); + EXPECT_TRUE(time_limit->LimitReached()); +} + +TEST(TimeLimitTest, ResetDeterministicTimeLimit) { + TimeLimit limit(/*limit_in_seconds=*/1.0, /*deterministic_limit=*/1.0); + EXPECT_FALSE(limit.LimitReached()); + limit.AdvanceDeterministicTime(2.0); + EXPECT_TRUE(limit.LimitReached()); + limit.ChangeDeterministicLimit(4.0); + EXPECT_FALSE(limit.LimitReached()); + EXPECT_EQ(limit.GetElapsedDeterministicTime(), 2.0); + EXPECT_EQ(limit.GetDeterministicTimeLeft(), 2.0); + EXPECT_EQ(limit.GetDeterministicLimit(), 4.0); +} + +TEST(SharedTimeLimitTest, IsThreadSafe) { + absl::BitGen gen; + + const int num_workers = 100; + const int num_repeats = 10; + + int total_time = 0; + TimeLimit time_limit; + SharedTimeLimit shared_time_limit(&time_limit); + { + ThreadPool pool("SharedTimeLimitTest", num_workers); + + for (int i = 0; i < num_workers; ++i) { + const int deterministic_ticks = + absl::Uniform(absl::IntervalClosedClosed, gen, 1, 100); + total_time += num_repeats * deterministic_ticks; + pool.Schedule([&shared_time_limit, deterministic_ticks]() { + for (int j = 0; j < num_repeats; ++j) { + shared_time_limit.AdvanceDeterministicTime(deterministic_ticks); + absl::SleepFor(absl::Milliseconds(deterministic_ticks)); + } + }); + } + } + EXPECT_EQ(total_time, shared_time_limit.GetElapsedDeterministicTime()); +} + +TEST(TimeLimitCheckEveryNCallsTest, WaitNBeforeFirstCheck) { + TimeLimit limit; + std::atomic stop = false; + limit.RegisterExternalBooleanAsLimit(&stop); + TimeLimitCheckEveryNCalls checker(10, &limit); + stop = true; + + for (int i = 0; i < 10; ++i) { + EXPECT_FALSE(checker.LimitReached()); + } + EXPECT_TRUE(checker.LimitReached()); +} + +TEST(TimeLimitCheckEveryNCallsTest, StickyTrue) { + TimeLimit limit; + std::atomic stop = false; + limit.RegisterExternalBooleanAsLimit(&stop); + TimeLimitCheckEveryNCalls checker(10, &limit); + EXPECT_FALSE(checker.LimitReached()); + stop = true; + + for (int i = 0; i < 9; ++i) { + EXPECT_FALSE(checker.LimitReached()); + } + + // Stays true forwever. + EXPECT_TRUE(checker.LimitReached()); + EXPECT_TRUE(checker.LimitReached()); + EXPECT_TRUE(checker.LimitReached()); +} + +} // namespace +} // namespace operations_research diff --git a/ortools/util/tuple_set_test.cc b/ortools/util/tuple_set_test.cc new file mode 100644 index 00000000000..c84beed5c36 --- /dev/null +++ b/ortools/util/tuple_set_test.cc @@ -0,0 +1,236 @@ +// 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/util/tuple_set.h" + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace operations_research { +namespace { + +TEST(IntTupleSetTest, SimpleFlow) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + set.Insert3(2, 4, 5); + set.Insert3(2, 4, 7); + EXPECT_EQ(4, set.NumTuples()); + EXPECT_EQ(3, set.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 6}; + std::vector tuple3 = {1, 3, 16}; + std::vector tuple4 = {1, 3, 16}; + EXPECT_TRUE(set.Contains(tuple1)); + EXPECT_TRUE(set.Contains(tuple2)); + EXPECT_FALSE(set.Contains(tuple3)); + EXPECT_FALSE(set.Contains(tuple4)); +} + +TEST(IntTupleSetTest, OneCopy) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + EXPECT_EQ(2, set2.NumTuples()); + EXPECT_EQ(3, set2.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 16}; + EXPECT_TRUE(set2.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple2)); +} + +TEST(IntTupleSetTest, OneCopyWithEqual) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2 = set; + EXPECT_EQ(2, set2.NumTuples()); + EXPECT_EQ(3, set2.Arity()); + std::vector tuple1 = {1, 3, 6}; + std::vector tuple2 = {1, 3, 16}; + EXPECT_TRUE(set2.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple2)); +} + +TEST(IntTupleSetTest, CopyAndBranchOnOriginal) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + set.Insert3(3, 3, 3); + EXPECT_EQ(3, set.NumTuples()); + EXPECT_EQ(2, set2.NumTuples()); + std::vector tuple1 = {3, 3, 3}; + EXPECT_TRUE(set.Contains(tuple1)); + EXPECT_FALSE(set2.Contains(tuple1)); +} + +TEST(IntTupleSetTest, CopyAndBranchOnCopy) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + IntTupleSet set2(set); + set2.Insert3(3, 3, 3); + EXPECT_EQ(2, set.NumTuples()); + EXPECT_EQ(3, set2.NumTuples()); + std::vector tuple1 = {3, 3, 3}; + EXPECT_FALSE(set.Contains(tuple1)); + EXPECT_TRUE(set2.Contains(tuple1)); +} + +TEST(IntTupleSetTest, MultipleCopiesAndDeletions) { + std::unique_ptr set(new IntTupleSet(3)); + set->Insert3(1, 3, 5); + set->Insert3(1, 3, 5); + set->Insert3(2, 4, 6); + std::unique_ptr set2(new IntTupleSet(*set)); + std::unique_ptr set3(new IntTupleSet(*set2)); + set2->Insert3(3, 3, 3); + + std::vector tuple1 = {3, 3, 3}; + EXPECT_EQ(2, set->NumTuples()); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_EQ(2, set3->NumTuples()); + EXPECT_FALSE(set->Contains(tuple1)); + EXPECT_TRUE(set2->Contains(tuple1)); + EXPECT_FALSE(set3->Contains(tuple1)); + + // Delete 'set' + set.reset(nullptr); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_EQ(2, set3->NumTuples()); + EXPECT_TRUE(set2->Contains(tuple1)); + EXPECT_FALSE(set3->Contains(tuple1)); + + // Delete 'set3' + set3.reset(nullptr); + EXPECT_EQ(3, set2->NumTuples()); + EXPECT_TRUE(set2->Contains(tuple1)); +} + +TEST(IntTupleSetTest, ZeroArity) { + IntTupleSet set(0); + EXPECT_EQ(0, set.Arity()); + EXPECT_EQ(0, set.NumTuples()); + std::vector empty_tuple; + EXPECT_FALSE(set.Contains(empty_tuple)); + EXPECT_EQ(0, set.Insert(empty_tuple)); + EXPECT_EQ(1, set.NumTuples()); + EXPECT_TRUE(set.Contains(empty_tuple)); + EXPECT_EQ(-1, set.Insert(empty_tuple)); +} + +TEST(IntTupleSetTest, NumDifferentValuesInColumn) { + IntTupleSet set(3); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 5); + set.Insert3(1, 3, 6); + set.Insert3(2, 4, 5); + set.Insert3(2, 4, 7); + EXPECT_EQ(2, set.NumDifferentValuesInColumn(0)); + EXPECT_EQ(2, set.NumDifferentValuesInColumn(1)); + EXPECT_EQ(3, set.NumDifferentValuesInColumn(2)); + EXPECT_EQ(0, set.NumDifferentValuesInColumn(-1)); + EXPECT_EQ(0, set.NumDifferentValuesInColumn(3)); +} + +TEST(IntTupleSetTest, SortedTupleSet) { + IntTupleSet set(3); + set.Insert3(1, 2, 5); + set.Insert3(1, 4, 5); + set.Insert3(1, 5, 6); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + EXPECT_EQ(5, set.NumDifferentValuesInColumn(1)); + IntTupleSet sorted = set.SortedByColumn(1); + EXPECT_EQ(0, sorted.Value(0, 1)); + EXPECT_EQ(2, sorted.Value(1, 1)); + EXPECT_EQ(3, sorted.Value(2, 1)); + EXPECT_EQ(4, sorted.Value(3, 1)); + EXPECT_EQ(5, sorted.Value(4, 1)); + EXPECT_EQ(2, set.Value(0, 1)); + EXPECT_EQ(4, set.Value(1, 1)); + EXPECT_EQ(5, set.Value(2, 1)); + EXPECT_EQ(3, set.Value(3, 1)); + EXPECT_EQ(0, set.Value(4, 1)); +} + +TEST(IntTupleSetTest, SortedTupleSetStability) { + IntTupleSet set(3); + set.Insert3(1, 5, 6); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + set.Insert3(1, 4, 5); + set.Insert3(1, 2, 5); + EXPECT_EQ(5, set.NumDifferentValuesInColumn(1)); + IntTupleSet sorted = set.SortedByColumn(0); + EXPECT_EQ(5, sorted.Value(0, 1)); + EXPECT_EQ(4, sorted.Value(1, 1)); + EXPECT_EQ(2, sorted.Value(2, 1)); + EXPECT_EQ(3, sorted.Value(3, 1)); + EXPECT_EQ(0, sorted.Value(4, 1)); +} + +void Expect3(const IntTupleSet& set, int index, int64_t v0, int64_t v1, + int64_t v2) { + EXPECT_EQ(v0, set.Value(index, 0)); + EXPECT_EQ(v1, set.Value(index, 1)); + EXPECT_EQ(v2, set.Value(index, 2)); +} + +TEST(IntTupleSetTest, LexicographicallySortedTupleSet) { + IntTupleSet set(3); + set.Insert3(1, 2, 5); + set.Insert3(1, 4, 6); + set.Insert3(1, 5, 6); + set.Insert3(1, 4, 5); + set.Insert3(2, 3, 5); + set.Insert3(2, 0, 7); + IntTupleSet sorted = set.SortedLexicographically(); + Expect3(sorted, 0, 1, 2, 5); + Expect3(sorted, 1, 1, 4, 5); + Expect3(sorted, 2, 1, 4, 6); + Expect3(sorted, 3, 1, 5, 6); + Expect3(sorted, 4, 2, 0, 7); + Expect3(sorted, 5, 2, 3, 5); +} + +TEST(IntTupleSetTest, InsertReturnValue) { + IntTupleSet set(2); + EXPECT_EQ(0, set.Insert2(0, 1)); + EXPECT_EQ(1, set.Insert2(1, 0)); + EXPECT_EQ(-1, set.Insert2(0, 1)); + EXPECT_EQ(2, set.Insert2(1, 1)); + EXPECT_EQ(-1, set.Insert2(1, 0)); +} + +TEST(IntTupleSetDeathTest, WrongArity) { + IntTupleSet set(3); + EXPECT_DEATH(set.Insert2(2, 4), ""); + EXPECT_DEATH(set.Insert4(2, 4, 4, 0), ""); +} +} // namespace +} // namespace operations_research diff --git a/ortools/util/zvector_test.cc b/ortools/util/zvector_test.cc new file mode 100644 index 00000000000..2ccd24921d0 --- /dev/null +++ b/ortools/util/zvector_test.cc @@ -0,0 +1,171 @@ +// 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/util/zvector.h" + +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "gtest/gtest.h" + +namespace operations_research { + +template +void TestZVectorZeroIndex() { + constexpr int64_t kZeroIndex = 0; + constexpr int64_t kNonZeroIndex = 10; + constexpr std::array kArrayBounds = {std::array{kZeroIndex, kZeroIndex}, + std::array{kZeroIndex, kNonZeroIndex}, + std::array{-kNonZeroIndex, kZeroIndex}}; + const T kValue = 1; + for (int64_t i = 0; i < kArrayBounds.size(); ++i) { + ZVector array(kArrayBounds[i][0], kArrayBounds[i][1]); + array.Set(kZeroIndex, kValue); + EXPECT_EQ(kValue, array[kZeroIndex]); + } +} + +TEST(ZVectorTest, SingleZeroIndex) { + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); + TestZVectorZeroIndex(); +} + +template +void TestZeroLengthZVector() { + ZVector a; + a.SetAll(1); +} + +TEST(ZVectorTest, ZeroLength) { + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); + TestZeroLengthZVector(); +} + +template +struct make_unsigned; +template <> +struct make_unsigned { + typedef uint8_t type; +}; +template <> +struct make_unsigned { + typedef uint16_t type; +}; +template <> +struct make_unsigned { + typedef uint32_t type; +}; +template <> +struct make_unsigned { + typedef uint64_t type; +}; +template <> +struct make_unsigned { + typedef uint8_t type; +}; +template <> +struct make_unsigned { + typedef uint16_t type; +}; +template <> +struct make_unsigned { + typedef uint32_t type; +}; +template <> +struct make_unsigned { + typedef uint64_t type; +}; + +template +T Random(std::mt19937* const randomizer) { + // Using the unsigned version of the type guarantees modular arithmetic, + // and avoids overflow on things such as INT_MAX - INT_MIN. + typedef typename make_unsigned::type U; + const U kMin = std::numeric_limits::min(); + const U kMax = std::numeric_limits::max(); + return static_cast(kMin + + absl::Uniform(*randomizer, 0, kMax - kMin)); +} + +template +void TestZVectorRangeValue() { + const int64_t kMinIndex = -100000; + const int64_t kMaxIndex = 100000; + + ZVector array(kMinIndex, kMaxIndex); + std::mt19937 randomizer(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + T value = Random(&randomizer); + array.Set(i, value); + } + randomizer.seed(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + T value = Random(&randomizer); + EXPECT_EQ(value, array[i]); + EXPECT_EQ(value, array.Value(i)); + } + array.SetAll(0); + for (int64_t i = kMinIndex; i <= kMaxIndex; ++i) { + EXPECT_EQ(0, array[i]); + EXPECT_EQ(0, array.Value(i)); + } +} + +TEST(ZVectorTest, Random) { + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); + TestZVectorRangeValue(); +} + +#ifndef NDEBUG +// Death depends on DCHECKs, so we do not check for death in opt mode. +TEST(ZVectorDeathTest, ReverseBounds) { + const int64_t kMinIndex = 1000; + const int64_t kMaxIndex = -1000; + EXPECT_DEATH({ ZVector(kMinIndex, kMaxIndex); }, ""); +} + +TEST(ZVectorDeathTest, BoundOverflow) { + const int64_t kMinIndex = -1000; + const int64_t kMaxIndex = 1000; + ZVector array = ZVector(kMinIndex, kMaxIndex); + EXPECT_DEATH({ array.Set(kMaxIndex + 1, 12); }, ""); + EXPECT_DEATH({ array.Value(kMaxIndex + 1); }, ""); + EXPECT_DEATH({ array[kMaxIndex + 1]; }, ""); + EXPECT_DEATH({ array.Set(kMinIndex - 1, 12); }, ""); + EXPECT_DEATH({ array.Value(kMinIndex - 1); }, ""); + EXPECT_DEATH({ array[kMinIndex - 1]; }, ""); +} +#endif + +} // namespace operations_research