diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index b0c3dd4e12..2f2d2cb9c7 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -285,7 +285,7 @@ cc_library( deps = [ "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/base:nullability", - "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/hash", "@abseil-cpp//absl/log:check", ], @@ -299,6 +299,7 @@ cc_test( "//ortools/base:gmock_main", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/hash", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:distributions", diff --git a/ortools/algorithms/space_saving_most_frequent.h b/ortools/algorithms/space_saving_most_frequent.h index 8cfa595de9..c3dc1674f9 100644 --- a/ortools/algorithms/space_saving_most_frequent.h +++ b/ortools/algorithms/space_saving_most_frequent.h @@ -22,7 +22,7 @@ #include "absl/base/attributes.h" #include "absl/base/nullability.h" -#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/hash/hash.h" #include "absl/log/check.h" @@ -59,8 +59,6 @@ class DoubleLinkedList; // http://dimacs.rutgers.edu/~graham/pubs/papers/freqvldbj.pdf // // This class is thread-compatible. -// -// TODO(user): Support move-only types. template , typename Eq = std::equal_to> class SpaceSavingMostFrequent { @@ -81,6 +79,9 @@ class SpaceSavingMostFrequent { // Complexity: O(1). void FullyRemove(const T& value); + // Returns the `num_samples` most frequent elements in the data structure + // sorted by decreasing count. Note: this does not work with non-copyable + // types. // TODO(user): Replace this by an iterator with a begin() and end(). std::vector> GetMostFrequent(int num_samples) const; @@ -120,6 +121,12 @@ class SpaceSavingMostFrequent { } } + void RemoveFromLinkedList(Item* absl_nonnull item) { + Bucket* absl_nonnull bucket = item->bucket; + item_alloc_.Return(bucket->items.erase(item)); + RemoveIfEmpty(bucket); + } + Bucket* absl_nonnull GetBucketForCountOne() { if (!buckets_.empty() && buckets_.back()->count == 1) { return buckets_.back(); @@ -134,7 +141,29 @@ class SpaceSavingMostFrequent { ssmf_internal::BoundedAllocator item_alloc_; ssmf_internal::BoundedAllocator bucket_alloc_; BucketList buckets_; // front with highest count. - absl::flat_hash_map elem_to_item_; + + struct HashItemPtr { + using is_transparent = void; + size_t operator()(const Item* absl_nonnull value) const { + return Hash()(value->value); + } + size_t operator()(const T& value) const { return Hash()(value); } + }; + + struct EqItemPtr { + using is_transparent = void; + bool operator()(const Item* absl_nonnull a, + const Item* absl_nonnull b) const { + return Eq()(a->value, b->value); + } + bool operator()(const Item* absl_nonnull a, const T& b) const { + return Eq()(a->value, b); + } + bool operator()(const T& a, const Item* absl_nonnull b) const { + return Eq()(a, b->value); + } + }; + absl::flat_hash_set item_ptr_set_; }; template @@ -143,7 +172,7 @@ SpaceSavingMostFrequent::SpaceSavingMostFrequent(int storage_size) item_alloc_(storage_size), bucket_alloc_(storage_size + 1) { CHECK_GT(storage_size, 0); - elem_to_item_.reserve(storage_size + 1); + item_ptr_set_.reserve(2 * storage_size); } // Properly return all buckets and items to their allocators to ensure proper @@ -169,21 +198,21 @@ void SpaceSavingMostFrequent::Add(T value) { if (buckets_.empty()) { // We are adding an element to an empty data structure. DCHECK(item_alloc_.empty()); - DCHECK(elem_to_item_.empty()); + DCHECK(item_ptr_set_.empty()); Bucket* absl_nonnull bucket = buckets_.insert_back(bucket_alloc_.New()); Item* absl_nonnull const item = bucket->items.insert_front(item_alloc_.New()); item->bucket = bucket; - item->value = value; + item->value = std::move(value); bucket->count = 1; - elem_to_item_.emplace(value, item); + item_ptr_set_.emplace(item); return; } DCHECK(!buckets_.empty()); - auto [it, inserted] = elem_to_item_.try_emplace(value); - if (inserted) { + auto it = item_ptr_set_.find(value); + if (it == item_ptr_set_.end()) { // We are adding a new element. First, check if we are full, and if so, // remove the least frequent element. if (item_alloc_.full()) { @@ -193,18 +222,18 @@ void SpaceSavingMostFrequent::Add(T value) { // the real least frequent of the bucket since it was unseen for longer. Item* absl_nonnull recycled_item = last_bucket->items.front(); // Reclaim its storage for the newly added element. - elem_to_item_.erase(recycled_item->value); + item_ptr_set_.erase(recycled_item); item_alloc_.Return(last_bucket->items.pop_front()); RemoveIfEmpty(last_bucket); } Bucket* absl_nonnull bucket = GetBucketForCountOne(); DCHECK_EQ(bucket->count, 1); Item* absl_nonnull item = bucket->items.insert_back(item_alloc_.New()); - item->value = value; + item->value = std::move(value); item->bucket = bucket; - it->second = item; // set item pointer back in map. + item_ptr_set_.emplace_hint(it, item); } else { - Item* absl_nonnull item = it->second; + Item* absl_nonnull item = *it; Bucket* absl_nonnull bucket = item->bucket; ItemList& current_bucket_items = bucket->items; const int64_t new_count = bucket->count + 1; @@ -239,13 +268,9 @@ void SpaceSavingMostFrequent::Add(T value) { template void SpaceSavingMostFrequent::FullyRemove(const T& value) { - auto it = elem_to_item_.find(value); - if (it == elem_to_item_.end()) return; - Item* absl_nonnull item = it->second; - Bucket* absl_nonnull bucket = item->bucket; - item_alloc_.Return(bucket->items.erase(item)); - RemoveIfEmpty(bucket); - elem_to_item_.erase(it); + auto node = item_ptr_set_.extract(value); + if (node.empty()) return; + RemoveFromLinkedList(node.value()); } template @@ -269,8 +294,11 @@ SpaceSavingMostFrequent::GetMostFrequent(int num_samples) const { template T SpaceSavingMostFrequent::PopMostFrequent() { CHECK(!buckets_.empty()); - const T value = buckets_.front()->items.back()->value; - FullyRemove(value); + Item* absl_nonnull item = buckets_.front()->items.back(); + DCHECK(item_ptr_set_.contains(item)); + item_ptr_set_.erase(item); + T value = std::move(item->value); + RemoveFromLinkedList(item); return value; } diff --git a/ortools/algorithms/space_saving_most_frequent_test.cc b/ortools/algorithms/space_saving_most_frequent_test.cc index 157da6b540..2f0949f532 100644 --- a/ortools/algorithms/space_saving_most_frequent_test.cc +++ b/ortools/algorithms/space_saving_most_frequent_test.cc @@ -13,8 +13,10 @@ #include "ortools/algorithms/space_saving_most_frequent.h" +#include #include #include +#include #include #include #include @@ -24,6 +26,7 @@ #include "absl/algorithm/container.h" #include "absl/base/nullability.h" +#include "absl/hash/hash.h" #include "absl/log/check.h" #include "absl/random/distributions.h" #include "absl/random/random.h" @@ -416,6 +419,55 @@ TEST(SpaceSavingMostFrequent, RandomInstances) { } } +TEST(SpaceSavingMostFrequent, WorksWithUniquePtr) { + SpaceSavingMostFrequentNaive naive_most_frequent(5); + + struct StringPtrHash { + std::size_t operator()(const std::unique_ptr& s) const { + return absl::Hash()(*s); + } + }; + struct StringPtrEq { + bool operator()(const std::unique_ptr& a, + const std::unique_ptr& b) const { + return *a == *b; + } + }; + SpaceSavingMostFrequent, StringPtrHash, + StringPtrEq> + most_frequent(5); + + auto add = [&](const std::string& value) { + most_frequent.Add(std::make_unique(value)); + naive_most_frequent.Add(value); + }; + + add("a"); + add("b"); + add("c"); + add("d"); + add("e"); + add("a"); + add("a"); + add("a"); + + add("b"); + add("c"); + add("d"); + add("e"); + add("f"); + add("g"); + + std::vector> res; + for (int i = 0; i < 10; ++i) { + const int64_t count = most_frequent.CountOfMostFrequent(); + if (count == 0) break; + res.push_back({*most_frequent.PopMostFrequent(), count}); + } + + EXPECT_EQ(res, naive_most_frequent.GetMostFrequent(10)); +} + template struct Element { Element() = default; diff --git a/ortools/constraint_solver/routing_sat.cc b/ortools/constraint_solver/routing_sat.cc index db44c39282..a2cd72b746 100644 --- a/ortools/constraint_solver/routing_sat.cc +++ b/ortools/constraint_solver/routing_sat.cc @@ -53,8 +53,6 @@ using operations_research::sat::CpObjectiveProto; using operations_research::sat::CpSolverResponse; using operations_research::sat::CpSolverStatus; using operations_research::sat::IntegerVariableProto; -using operations_research::sat::kMaxIntegerValue; -using operations_research::sat::kMinIntegerValue; using operations_research::sat::LinearConstraintProto; using operations_research::sat::Model; using operations_research::sat::NewSatParameters; diff --git a/ortools/glpk/BUILD.bazel b/ortools/glpk/BUILD.bazel index 61c1a17494..c48f720a35 100644 --- a/ortools/glpk/BUILD.bazel +++ b/ortools/glpk/BUILD.bazel @@ -14,7 +14,10 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") -package(default_visibility = ["//visibility:public"]) +package( + # Code specific to Glpk solver used by linear_solver/ and math_opt/. + default_visibility = ["//visibility:public"], +) cc_library( name = "glpk_env_deleter", diff --git a/ortools/gurobi/isv_public/BUILD.bazel b/ortools/gurobi/isv_public/BUILD.bazel index 1950a31c67..e8d43f9cb7 100644 --- a/ortools/gurobi/isv_public/BUILD.bazel +++ b/ortools/gurobi/isv_public/BUILD.bazel @@ -20,10 +20,10 @@ cc_library( srcs = ["gurobi_isv.cc"], hdrs = ["gurobi_isv.h"], deps = [ - "//ortools/base:status_builder", "//ortools/base:status_macros", "//ortools/math_opt/solvers:gurobi_cc_proto", "//ortools/third_party_solvers:gurobi_environment", + "@abseil-cpp//absl/cleanup", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", diff --git a/ortools/linear_solver/testdata/BUILD.bazel b/ortools/linear_solver/testdata/BUILD.bazel index f90dc5e671..8678b2f8b5 100644 --- a/ortools/linear_solver/testdata/BUILD.bazel +++ b/ortools/linear_solver/testdata/BUILD.bazel @@ -11,10 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Test files for the linear_solver packages. +# Test files for the linear_solver component. +package(default_visibility = ["//visibility:public"]) -exports_files([ - "maximization.mps", - "small_model.lp", - "large_model.mps.gz", -]) +exports_files(glob(["*"])) diff --git a/ortools/math_opt/core/python/BUILD.bazel b/ortools/math_opt/core/python/BUILD.bazel index c372b6d0fd..42d556ae13 100644 --- a/ortools/math_opt/core/python/BUILD.bazel +++ b/ortools/math_opt/core/python/BUILD.bazel @@ -18,6 +18,7 @@ package(default_visibility = ["//ortools/math_opt:__subpackages__"]) pybind_extension( name = "solver", srcs = ["solver.cc"], + visibility = ["//visibility:public"], deps = select({ "//ortools/linear_solver:use_cp_sat": ["//ortools/math_opt/solvers:cp_sat_solver"], diff --git a/ortools/math_opt/elemental/python/BUILD.bazel b/ortools/math_opt/elemental/python/BUILD.bazel index 207be61169..92818cda83 100644 --- a/ortools/math_opt/elemental/python/BUILD.bazel +++ b/ortools/math_opt/elemental/python/BUILD.bazel @@ -39,7 +39,7 @@ pybind_extension( srcs = [ "elemental.cc", ], - visibility = ["//ortools/math_opt/python:__subpackages__"], + visibility = ["//visibility:public"], deps = [ "//ortools/base:status_macros", "//ortools/math_opt/elemental", diff --git a/ortools/math_opt/io/python/BUILD.bazel b/ortools/math_opt/io/python/BUILD.bazel index 08967d111a..bf003b9dd2 100644 --- a/ortools/math_opt/io/python/BUILD.bazel +++ b/ortools/math_opt/io/python/BUILD.bazel @@ -20,6 +20,7 @@ package(default_visibility = ["//visibility:public"]) pybind_extension( name = "mps_converter", srcs = ["mps_converter.cc"], + visibility = ["//visibility:public"], deps = [ "//ortools/math_opt:model_cc_proto", diff --git a/ortools/packing/BUILD.bazel b/ortools/packing/BUILD.bazel index 675c96cf14..1947eff697 100644 --- a/ortools/packing/BUILD.bazel +++ b/ortools/packing/BUILD.bazel @@ -29,9 +29,11 @@ cc_library( "//ortools/base", "//ortools/base:map_util", "//ortools/base:stl_util", + "//ortools/base:types", "//ortools/graph:topologicalsorter", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", ], ) @@ -41,10 +43,12 @@ cc_library( hdrs = ["arc_flow_solver.h"], deps = [ ":arc_flow_builder", + ":vector_bin_packing_cc_proto", "//ortools/base", "//ortools/base:file", + "//ortools/base:timer", "//ortools/linear_solver", - "//ortools/packing:vector_bin_packing_cc_proto", + "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/flags:flag", ], ) @@ -54,12 +58,10 @@ cc_library( proto_library( name = "vector_bin_packing_proto", srcs = ["vector_bin_packing.proto"], - visibility = ["//visibility:public"], ) cc_proto_library( name = "vector_bin_packing_cc_proto", - visibility = ["//visibility:public"], deps = [":vector_bin_packing_proto"], ) @@ -67,10 +69,9 @@ cc_library( name = "vector_bin_packing_parser", srcs = ["vector_bin_packing_parser.cc"], hdrs = ["vector_bin_packing_parser.h"], - visibility = ["//visibility:public"], deps = [ ":vector_bin_packing_cc_proto", - "//ortools/base", + "//ortools/base:types", "//ortools/util:filelineiter", "@abseil-cpp//absl/strings", ], @@ -80,14 +81,15 @@ cc_binary( name = "vector_bin_packing", srcs = ["vector_bin_packing_main.cc"], deps = [ + ":arc_flow_solver", + ":vector_bin_packing_cc_proto", + ":vector_bin_packing_parser", "//ortools/base", "//ortools/base:file", - "//ortools/packing:arc_flow_builder", - "//ortools/packing:arc_flow_solver", - "//ortools/packing:vector_bin_packing_cc_proto", - "//ortools/packing:vector_bin_packing_parser", + "//ortools/linear_solver", "@abseil-cpp//absl/flags:flag", - "@abseil-cpp//absl/status", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:globals", "@abseil-cpp//absl/strings", ], ) @@ -116,12 +118,10 @@ cc_test( proto_library( name = "multiple_dimensions_bin_packing_proto", srcs = ["multiple_dimensions_bin_packing.proto"], - visibility = ["//visibility:public"], ) cc_proto_library( name = "multiple_dimensions_bin_packing_cc_proto", - visibility = ["//visibility:public"], deps = [":multiple_dimensions_bin_packing_proto"], ) @@ -129,10 +129,10 @@ cc_library( name = "binpacking_2d_parser", srcs = ["binpacking_2d_parser.cc"], hdrs = ["binpacking_2d_parser.h"], - visibility = ["//visibility:public"], deps = [ ":multiple_dimensions_bin_packing_cc_proto", "//ortools/base", + "//ortools/base:types", "//ortools/util:filelineiter", "@abseil-cpp//absl/strings", ], diff --git a/ortools/packing/testdata/BUILD.bazel b/ortools/packing/testdata/BUILD.bazel index b518646807..a5b6170f60 100644 --- a/ortools/packing/testdata/BUILD.bazel +++ b/ortools/packing/testdata/BUILD.bazel @@ -11,11 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -package(default_visibility = ["//visibility:public"]) - -exports_files( - [ - "1D__bpp_scholl__bin2data.N2W2B1R0.vbp", - "Class_01.2bp", - ], -) +exports_files([ + "1D__bpp_scholl__bin2data.N2W2B1R0.vbp", + "Class_01.2bp", +]) diff --git a/ortools/pdlp/BUILD.bazel b/ortools/pdlp/BUILD.bazel index d2ac85c65b..10283bda2a 100644 --- a/ortools/pdlp/BUILD.bazel +++ b/ortools/pdlp/BUILD.bazel @@ -27,7 +27,7 @@ cc_library( ":solvers_cc_proto", "//ortools/base:threadpool", "@abseil-cpp//absl/functional:any_invocable", - "@abseil-cpp//absl/log", + "@abseil-cpp//absl/synchronization", "@eigen", ], ) @@ -40,7 +40,7 @@ cc_test( ":scheduler", ":solvers_cc_proto", "//ortools/base:gmock_main", - "@abseil-cpp//absl/functional:any_invocable", + "@abseil-cpp//absl/log", ], ) @@ -73,9 +73,7 @@ cc_proto_library( py_proto_library( name = "solvers_py_pb2", - deps = [ - ":solvers_proto", - ], + deps = [":solvers_proto"], ) cc_library( @@ -88,8 +86,8 @@ cc_library( ":sharder", ":solve_log_cc_proto", ":solvers_cc_proto", - "//ortools/base", "//ortools/base:mathutil", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@eigen", ], @@ -107,7 +105,7 @@ cc_test( ":solvers_cc_proto", ":test_util", "//ortools/base:gmock_main", - "//ortools/base:protobuf_util", + "//ortools/base:parse_text_proto", "@eigen", ], ) @@ -127,7 +125,6 @@ cc_library( ":solvers_proto_validation", ":termination", ":trust_region", - "//ortools/base", "//ortools/base:mathutil", "//ortools/base:timer", "//ortools/glop:parameters_cc_proto", @@ -139,6 +136,8 @@ cc_library( "//ortools/util:logging", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", @@ -164,13 +163,14 @@ cc_test( ":solvers_cc_proto", ":termination", ":test_util", - "//ortools/base", "//ortools/base:gmock_main", "//ortools/glop:parameters_cc_proto", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/lp_data", "//ortools/lp_data:base", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@eigen", @@ -182,9 +182,9 @@ cc_library( srcs = ["quadratic_program.cc"], hdrs = ["quadratic_program.h"], deps = [ - "//ortools/base", "//ortools/base:status_macros", "//ortools/linear_solver:linear_solver_cc_proto", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", @@ -200,8 +200,7 @@ cc_test( ":quadratic_program", ":test_util", "//ortools/base:gmock_main", - "//ortools/base:protobuf_util", - "//ortools/base:status_macros", + "//ortools/base:parse_text_proto", "//ortools/linear_solver:linear_solver_cc_proto", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", @@ -215,8 +214,10 @@ cc_library( hdrs = ["quadratic_program_io.h"], deps = [ ":quadratic_program", - "//ortools/base", + "//ortools/base:file", + "//ortools/base:gzipfile", "//ortools/base:mathutil", + "//ortools/base:recordio", "//ortools/base:status_macros", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_exporter", @@ -224,6 +225,8 @@ cc_library( "//ortools/util:file_util", "@abseil-cpp//absl/base", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", @@ -241,8 +244,9 @@ cc_library( ":sharded_quadratic_program", ":sharder", ":solve_log_cc_proto", - "//ortools/base", "//ortools/base:mathutil", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random:distributions", "@eigen", ], @@ -274,9 +278,9 @@ cc_library( ":scheduler", ":sharder", ":solvers_cc_proto", - "//ortools/base", "//ortools/util:logging", - "@abseil-cpp//absl/memory", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/strings", "@eigen", ], @@ -302,7 +306,6 @@ cc_library( hdrs = ["sharder.h"], deps = [ ":scheduler", - "//ortools/base", "//ortools/base:mathutil", "//ortools/base:timer", "@abseil-cpp//absl/log", @@ -323,9 +326,9 @@ cc_test( ":scheduler", ":sharder", ":solvers_cc_proto", - "//ortools/base", "//ortools/base:gmock_main", "//ortools/base:mathutil", + "@abseil-cpp//absl/log", "@abseil-cpp//absl/random:distributions", "@eigen", ], @@ -350,7 +353,7 @@ cc_test( ":solvers_cc_proto", ":solvers_proto_validation", "//ortools/base:gmock_main", - "//ortools/base:protobuf_util", + "//ortools/base:parse_text_proto", "@abseil-cpp//absl/status", "@abseil-cpp//absl/strings", ], @@ -363,7 +366,7 @@ cc_library( deps = [ ":solve_log_cc_proto", ":solvers_cc_proto", - "//ortools/base", + "@abseil-cpp//absl/log", ], ) @@ -376,7 +379,7 @@ cc_test( ":solvers_cc_proto", ":termination", "//ortools/base:gmock_main", - "//ortools/base:protobuf_util", + "//ortools/base:parse_text_proto", ], ) @@ -387,8 +390,10 @@ cc_library( hdrs = ["test_util.h"], deps = [ ":quadratic_program", - "//ortools/base", "//ortools/base:gmock", + "//ortools/base:path", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/types:span", "@eigen", ], @@ -399,8 +404,8 @@ cc_test( srcs = ["test_util_test.cc"], deps = [ ":test_util", - "//ortools/base", "//ortools/base:gmock_main", + "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/types:span", "@eigen", ], @@ -415,9 +420,10 @@ cc_library( ":sharded_optimization_utils", ":sharded_quadratic_program", ":sharder", - "//ortools/base", "//ortools/base:mathutil", "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", "@eigen", ], ) diff --git a/ortools/pdlp/iteration_stats_test.cc b/ortools/pdlp/iteration_stats_test.cc index a1c4415521..50dc5404c7 100644 --- a/ortools/pdlp/iteration_stats_test.cc +++ b/ortools/pdlp/iteration_stats_test.cc @@ -21,7 +21,7 @@ #include "Eigen/Core" #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/protobuf_util.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/pdlp/quadratic_program.h" #include "ortools/pdlp/sharded_quadratic_program.h" #include "ortools/pdlp/solve_log.pb.h" @@ -31,16 +31,459 @@ namespace operations_research::pdlp { namespace { -using ::google::protobuf::util::ParseTextOrDie; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; using ::testing::AllOf; using ::testing::Each; using ::testing::ElementsAre; using ::testing::Eq; +using ::testing::EqualsProto; using ::testing::Ge; using ::testing::Le; using ::testing::Ne; using ::testing::SizeIs; +using ::testing::proto::Approximately; +using ::testing::proto::Partially; + +// The following block relies heavily on `EqualsProto`, which isn't open source. + +void CheckScaledAndUnscaledConvergenceInformation( + QuadraticProgram qp, const Eigen::VectorXd& primal_solution, + const Eigen::VectorXd& dual_solution, + const double componentwise_primal_residual_offset, + const double componentwise_dual_residual_offset, + const ConvergenceInformation& expected_stats) { + const int num_threads = 2; + const int num_shards = 10; + ShardedQuadraticProgram sharded_qp(std::move(qp), num_threads, num_shards); + EXPECT_THAT( + ComputeScaledConvergenceInformation( + PrimalDualHybridGradientParams(), sharded_qp, primal_solution, + dual_solution, componentwise_primal_residual_offset, + componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(expected_stats)))); + + // Rescale the problem so that the primal and dual solutions have elements + // equal to -1.0, 0.0, or 1.0. + Eigen::VectorXd col_scaling_vec = primal_solution.unaryExpr( + [](double x) { return x != 0.0 ? std::abs(x) : 1.0; }); + Eigen::VectorXd row_scaling_vec = dual_solution.unaryExpr( + [](double x) { return x != 0.0 ? std::abs(x) : 1.0; }); + Eigen::VectorXd scaled_primal_solution = + primal_solution.cwiseQuotient(col_scaling_vec); + Eigen::VectorXd scaled_dual_solution = + dual_solution.cwiseQuotient(row_scaling_vec); + sharded_qp.RescaleQuadraticProgram(col_scaling_vec, row_scaling_vec); + EXPECT_THAT( + ComputeConvergenceInformation( + PrimalDualHybridGradientParams(), sharded_qp, col_scaling_vec, + row_scaling_vec, scaled_primal_solution, scaled_dual_solution, + componentwise_primal_residual_offset, + componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(expected_stats)))); + + // Also check that the iteration stats for the scaled problem have the correct + // objectives and norms. + ConvergenceInformation expected_scaled_stats; + expected_scaled_stats.set_primal_objective(expected_stats.primal_objective()); + expected_scaled_stats.set_dual_objective(expected_stats.dual_objective()); + expected_scaled_stats.set_l_inf_primal_variable(1.0); + expected_scaled_stats.set_l_inf_dual_variable(1.0); + + EXPECT_THAT( + ComputeScaledConvergenceInformation( + PrimalDualHybridGradientParams(), sharded_qp, scaled_primal_solution, + scaled_dual_solution, componentwise_primal_residual_offset, + componentwise_dual_residual_offset, POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(expected_scaled_stats)))); +} + +void CheckScaledAndUnscaledInfeasibilityStats( + QuadraticProgram qp, const Eigen::VectorXd& primal_ray, + const Eigen::VectorXd& dual_ray, + const Eigen::VectorXd& primal_solution_for_residual_tests, + const InfeasibilityInformation& expected_infeasibility_info) { + const int num_threads = 2; + const int num_shards = 2; + ShardedQuadraticProgram sharded_qp(std::move(qp), num_threads, num_shards); + EXPECT_THAT( + ComputeInfeasibilityInformation( + PrimalDualHybridGradientParams(), sharded_qp, + Eigen::VectorXd::Ones(sharded_qp.PrimalSize()), + Eigen::VectorXd::Ones(sharded_qp.DualSize()), primal_ray, dual_ray, + primal_solution_for_residual_tests, POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(expected_infeasibility_info)))); + + // Rescale the problem so that the primal and dual certificates have elements + // equal to -1.0, 0.0, or 1.0. + Eigen::VectorXd col_scaling_vec = primal_ray.unaryExpr( + [](double x) { return x != 0.0 ? std::abs(x) : 1.0; }); + Eigen::VectorXd row_scaling_vec = + dual_ray.unaryExpr([](double x) { return x != 0.0 ? std::abs(x) : 1.0; }); + Eigen::VectorXd scaled_primal_solution = + primal_ray.cwiseQuotient(col_scaling_vec); + Eigen::VectorXd scaled_dual_solution = + dual_ray.cwiseQuotient(row_scaling_vec); + Eigen::VectorXd scaled_primal_solution_for_residual_tests = + primal_solution_for_residual_tests.cwiseQuotient(col_scaling_vec); + sharded_qp.RescaleQuadraticProgram(col_scaling_vec, row_scaling_vec); + EXPECT_THAT( + ComputeInfeasibilityInformation( + PrimalDualHybridGradientParams(), sharded_qp, col_scaling_vec, + row_scaling_vec, scaled_primal_solution, scaled_dual_solution, + scaled_primal_solution_for_residual_tests, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(expected_infeasibility_info)))); +} + +TEST(IterationStatsTest, SimpleLpAtOptimum) { + const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 2.5}}; + const Eigen::VectorXd dual_solution{{-2.0, 0.0, 2.375, 2.0 / 3}}; + CheckScaledAndUnscaledConvergenceInformation( + TestLp(), primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + ParseTextOrDie(R"pb( + primal_objective: -34.0 + dual_objective: -34.0 + corrected_dual_objective: -34.0 + l_inf_primal_residual: 0.0 + l2_primal_residual: 0.0 + l_inf_componentwise_primal_residual: 0.0 + l_inf_dual_residual: 0.0 + l2_dual_residual: 0.0 + l_inf_componentwise_dual_residual: 0.0 + l_inf_primal_variable: 8.0 + l2_primal_variable: 8.5 + l_inf_dual_variable: 2.375 + l2_dual_variable: 3.1756998353818715 + )pb")); +} + +TEST(IterationStatsTest, SimpleLpWithPrimalResidual) { + // This is the optimal solution, except that x_3 (`primal_solution[3]`) has + // been changed from 2.5 to 3.5, increasing the objective by 1, but causing + // the first constraint to be violated by 2 and the last constraint by 1. + const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 3.5}}; + const Eigen::VectorXd dual_solution{{-2.0, 0.0, 2.375, 2.0 / 3}}; + CheckScaledAndUnscaledConvergenceInformation( + TestLp(), primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + ParseTextOrDie(R"pb( + primal_objective: -33.0 + dual_objective: -34.0 + corrected_dual_objective: -34.0 + l_inf_primal_residual: 2.0 + l2_primal_residual: 2.2360679774997896 + l_inf_componentwise_primal_residual: 0.5 + l_inf_dual_residual: 0.0 + l2_dual_residual: 0.0 + l_inf_componentwise_dual_residual: 0.0 + l_inf_primal_variable: 8.0 + l2_primal_variable: 8.8459030064770662 + l_inf_dual_variable: 2.375 + l2_dual_variable: 3.1756998353818715 + )pb")); +} + +TEST(IterationStatsTest, SimpleLpWithDualResidual) { + // This is the optimal solution, except that y_1 (`dual_solution[1]`) has been + // changed from 0 to -1, causing x_0 and x_2 to have primal gradients (dual + // residuals) of 1.0. + const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 2.5}}; + const Eigen::VectorXd dual_solution{{-2.0, -1.0, 2.375, 2.0 / 3}}; + CheckScaledAndUnscaledConvergenceInformation( + TestLp(), primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + ParseTextOrDie(R"pb( + primal_objective: -34.0 + dual_objective: -41.0 + corrected_dual_objective: -inf + l_inf_primal_residual: 0.0 + l2_primal_residual: 0.0 + l_inf_componentwise_primal_residual: 0.0 + l_inf_dual_residual: 1.0 + l2_dual_residual: 1.4142135623730950 + l_inf_componentwise_dual_residual: 0.5 + l_inf_primal_variable: 8.0 + l2_primal_variable: 8.5 + l_inf_dual_variable: 2.375 + l2_dual_variable: 3.3294247918288294 + )pb")); +} + +TEST(IterationStatsTest, SimpleLpWithBothResiduals) { + // This is the optimal solution, except that x_3 (`primal_solution[3]`) has + // been changed from 2.5 to 3.5, increasing the objective by 1, but causing + // the first constraint to be violated by 2 and the last constraint by 1, and + // y_1 (`dual_solution[1]`) has been changed from 0 to -1, causing x_0 and x_2 + // to have primal gradients (dual residuals) of 1.0. The primal and dual + // componentwise_residual_offset values are different, to check that the + // correct offset is applied when computing the + // l_inf_componentwise_XXX_residual values. + const Eigen::VectorXd primal_solution{{-1.0, 8.0, 1.0, 3.5}}; + const Eigen::VectorXd dual_solution{{-2.0, -1.0, 2.375, 2.0 / 3}}; + CheckScaledAndUnscaledConvergenceInformation( + TestLp(), primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/3.0, + /*componentwise_dual_residual_offset=*/1.0, + ParseTextOrDie(R"pb( + primal_objective: -33.0 + dual_objective: -41.0 + corrected_dual_objective: -inf + l_inf_primal_residual: 2.0 + l2_primal_residual: 2.2360679774997896 + l_inf_componentwise_primal_residual: 0.25 + l_inf_dual_residual: 1.0 + l2_dual_residual: 1.4142135623730950 + l_inf_componentwise_dual_residual: 0.5 + l_inf_primal_variable: 8.0 + l2_primal_variable: 8.8459030064770662 + l_inf_dual_variable: 2.375 + l2_dual_variable: 3.3294247918288294 + )pb")); +} + +TEST(IterationStatsTest, SimpleQpAtOptimum) { + const Eigen::VectorXd primal_solution{{1.0, 0.0}}; + const Eigen::VectorXd dual_solution{{-1.0}}; + CheckScaledAndUnscaledConvergenceInformation( + TestDiagonalQp1(), primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + ParseTextOrDie(R"pb( + primal_objective: 6.0 + dual_objective: 6.0 + corrected_dual_objective: 6.0 + l_inf_primal_residual: 0.0 + l2_primal_residual: 0.0 + l_inf_componentwise_primal_residual: 0.0 + l_inf_dual_residual: 0.0 + l2_dual_residual: 0.0 + l_inf_componentwise_dual_residual: 0.0 + l_inf_primal_variable: 1.0 + l2_primal_variable: 1.0 + l_inf_dual_variable: 1.0 + l2_dual_variable: 1.0 + )pb")); +} + +TEST(IterationStatsTest, SimpleLpWithGapResidualsAndZeroPrimalSolution) { + const int num_threads = 2; + const int num_shards = 10; + ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards); + + const Eigen::VectorXd primal_solution = Eigen::VectorXd::Zero(4); + const Eigen::VectorXd dual_solution{{1.0, 0.0, 0.0, -1.0}}; + + PrimalDualHybridGradientParams params_true, params_false; + params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + true); + params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + false); + + // c is: [5.5, -2, -1, 1] + // -A^T y is: [-2, -1, 0.5, -3] + // c - A^T y is: [3.5, -3.0, -0.5, -2.0]. + // When the primal variable is 0.0 and the bound is not 0.0, the bound + // corresponding to c - A^T y is handled as infinite when + // `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true. + // Thus, for the all zero primal solution: when + // `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true, the + // residuals are [3.5, -3.0, -0.5, -2.0] and all bounds are treated as + // infinite. When `handle_some_primal_gradients_on_finite_bounds_as_residuals` + // is false, the residuals are [3.5, -3.0, 0, 0] and the corresponding bound + // terms are [0.0, -2, 6, 3.5]. + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_true, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: -3.0 + corrected_dual_objective: -inf + l_inf_dual_residual: 3.5 + # 5.0497524691810389 = L_2(3.5, -3.0, -0.5, -2.0) + l2_dual_residual: 5.0497524691810389 + )pb")))); + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_false, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: -7.0 + corrected_dual_objective: -inf + l_inf_dual_residual: 3.5 + # 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0) + l2_dual_residual: 4.6097722286464436 + )pb")))); +} + +TEST(IterationStatsTest, SimpleLpWithGapResidualsAndNonZeroPrimalSolution) { + const int num_threads = 2; + const int num_shards = 10; + ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards); + + const Eigen::VectorXd primal_solution{{0.0, 0.0, 4.0, 3.0}}; + const Eigen::VectorXd dual_solution{{1.0, 0.0, 0.0, -1.0}}; + + PrimalDualHybridGradientParams params_true, params_false; + params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + true); + params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + false); + + // c is: [5.5, -2, -1, 1] + // -A^T y is: [-2, -1, 0.5, -3] + // c - A^T y is: [3.5, -3.0, -0.5, -2.0]. + // When the primal variable is 0.0 and the bound is not 0.0, the bound + // corresponding to c - A^T y is treated as infinite when + // `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true. + // Thus, for primal_solution [0, 0, 4, 3], whether + // `handle_some_primal_gradients_on_finite_bounds_as_residuals` is true or + // not, the residuals are [3.5, -3.0, 0.0, 0.0] and the corresponding bound + // terms are [0.0, -2, 6, 3.5]. + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_true, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: -13.0 + corrected_dual_objective: -inf + l_inf_dual_residual: 3.5 + # 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0) + l2_dual_residual: 4.6097722286464436 + )pb")))); + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_false, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: -7.0 + corrected_dual_objective: -inf + l_inf_dual_residual: 3.5 + # 4.6097722286464436 = L_2(3.5, -3.0, 0.0, 0.0) + l2_dual_residual: 4.6097722286464436 + )pb")))); +} + +TEST(IterationStatsTest, SimpleQp) { + const int num_threads = 2; + const int num_shards = 10; + ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads, + num_shards); + + const Eigen::VectorXd primal_solution{{1.0, 2.0}}; + const Eigen::VectorXd dual_solution{{0.0}}; + PrimalDualHybridGradientParams params_true, params_false; + params_true.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + true); + params_false.set_handle_some_primal_gradients_on_finite_bounds_as_residuals( + false); + // Q*x is: [4.0, 2.0] + // c is: [-1, -1] + // A^T y is zero. + // If `handle_some_primal_gradients_on_finite_bounds_as_residuals` is + // true the second primal gradient term is handled as a residual, not a + // reduced cost. + // Other than the reduced cost terms, the dual objective is 5 (objective + // offset) - 4 (1/2 x^T Q x) = 1 + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_true, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: 8 + corrected_dual_objective: 2 + l_inf_dual_residual: 1.0 + l2_dual_residual: 1.0 + )pb")))); + EXPECT_THAT(ComputeScaledConvergenceInformation( + params_false, sharded_qp, primal_solution, dual_solution, + /*componentwise_primal_residual_offset=*/1.0, + /*componentwise_dual_residual_offset=*/1.0, + POINT_TYPE_CURRENT_ITERATE), + Partially(Approximately(EqualsProto(R"pb( + dual_objective: 2 + corrected_dual_objective: 2 + l_inf_dual_residual: 0.0 + l2_dual_residual: 0.0 + )pb")))); +} + +TEST(IterationStatsTest, InfeasibilityInformationWithCertificateLp) { + const Eigen::VectorXd primal_ray{{0.0, 0.0}}; + const Eigen::VectorXd dual_ray{{-1.0, -1.0}}; + CheckScaledAndUnscaledInfeasibilityStats( + SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray, + ParseTextOrDie(R"pb( + max_primal_ray_infeasibility: 0 + primal_ray_linear_objective: 0 + primal_ray_quadratic_norm: 0 + max_dual_ray_infeasibility: 0 + dual_ray_objective: 1 + )pb")); +} + +TEST(IterationStatsTest, InfeasibilityInformationWithoutCertificateLp) { + const Eigen::VectorXd primal_ray{{2.0, 1.0}}; + const Eigen::VectorXd dual_ray{{-1.0, -3.0}}; + CheckScaledAndUnscaledInfeasibilityStats( + SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray, + ParseTextOrDie(R"pb( + max_primal_ray_infeasibility: 0.5 + primal_ray_linear_objective: 1.5 + primal_ray_quadratic_norm: 0 + max_dual_ray_infeasibility: 0.66666666666666663 + dual_ray_objective: 1.6666666666666667 + )pb")); +} + +TEST(IterationStatsTest, DetectsDualRayHasInfeasibleComponent) { + const Eigen::VectorXd primal_ray{{0.0, 0.0}}; + const Eigen::VectorXd dual_ray{{1.0, 1.0}}; + // Components with the wrong sign cause the dual ray objective to be -inf. + CheckScaledAndUnscaledInfeasibilityStats( + SmallPrimalInfeasibleLp(), primal_ray, dual_ray, primal_ray, + ParseTextOrDie(R"pb( + max_dual_ray_infeasibility: 0.0 + dual_ray_objective: -inf + )pb")); +} + +// Regression test for failures of math_opt's +// SimpleLpTest.OptimalAfterInfeasible test. +TEST(IterationStatsTest, HandlesReducedCostsOnDualRayCorrectly) { + // A trivial LP mimicking the one used in math_opt's test: + // min x + // Constraint: 2 <= x + // Variable: 0 <= x <= 1 + QuadraticProgram lp(1, 1); + lp.objective_vector = Eigen::VectorXd{{1}}; + lp.constraint_lower_bounds = Eigen::VectorXd{{2}}; + lp.constraint_upper_bounds = + Eigen::VectorXd{{std::numeric_limits::infinity()}}; + lp.variable_lower_bounds = Eigen::VectorXd{{0}}; + lp.variable_upper_bounds = Eigen::VectorXd{{1}}; + lp.constraint_matrix.coeffRef(0, 0) = 1.0; + lp.constraint_matrix.makeCompressed(); + const Eigen::VectorXd primal_solution{{1.0}}; + const Eigen::VectorXd primal_ray{{0.0}}; + const Eigen::VectorXd dual_ray{{1.0}}; + // `dual_ray_objective` = 2 (objective term) - 1 (reduced cost on x) = 1. + CheckScaledAndUnscaledInfeasibilityStats( + lp, primal_ray, dual_ray, primal_solution, + ParseTextOrDie(R"pb( + max_dual_ray_infeasibility: 0.0 + dual_ray_objective: 1.0 + )pb")); +} TEST(CorrectedDualTest, SimpleLpWithSuboptimalDual) { const int num_threads = 2; diff --git a/ortools/pdlp/python/BUILD.bazel b/ortools/pdlp/python/BUILD.bazel index 423313a3f1..66475a4580 100644 --- a/ortools/pdlp/python/BUILD.bazel +++ b/ortools/pdlp/python/BUILD.bazel @@ -39,15 +39,13 @@ py_test( name = "pdlp_test", size = "small", srcs = ["pdlp_test.py"], - data = [ - ":pdlp.so", - ], + data = [":pdlp.so"], deps = [ + "//ortools/pdlp:solve_log_py_pb2", + "//ortools/pdlp:solvers_py_pb2", requirement("absl-py"), requirement("numpy"), requirement("scipy"), "//ortools/linear_solver:linear_solver_py_pb2", - "//ortools/pdlp:solve_log_py_pb2", - "//ortools/pdlp:solvers_py_pb2", ], ) diff --git a/ortools/pdlp/quadratic_program_test.cc b/ortools/pdlp/quadratic_program_test.cc index 28e36b9341..3e5c02390d 100644 --- a/ortools/pdlp/quadratic_program_test.cc +++ b/ortools/pdlp/quadratic_program_test.cc @@ -26,18 +26,19 @@ #include "absl/status/statusor.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/protobuf_util.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/pdlp/test_util.h" namespace operations_research::pdlp { namespace { -using ::google::protobuf::util::ParseTextOrDie; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; using ::operations_research::pdlp::internal::CombineRepeatedTripletsInPlace; using ::testing::ElementsAre; using ::testing::EndsWith; using ::testing::Eq; +using ::testing::EqualsProto; using ::testing::HasSubstr; using ::testing::IsEmpty; using ::testing::Optional; @@ -45,6 +46,7 @@ using ::testing::PrintToString; using ::testing::SizeIs; using ::testing::StartsWith; using ::testing::StrEq; +using ::testing::status::IsOkAndHolds; const double kInfinity = std::numeric_limits::infinity(); @@ -231,6 +233,27 @@ TEST_P(ConvertQpMpModelProtoTest, LpFromMpModelProto) { VerifyTestLp(*lp, maximize); } +TEST_P(ConvertQpMpModelProtoTest, LpToMpModelProto) { + const bool maximize = GetParam(); + QuadraticProgram lp = TestLp(); + if (maximize) { + lp.objective_scaling_factor = -1; + lp.objective_vector *= -1; + lp.objective_offset *= -1; + } + EXPECT_THAT(QpToMpModelProto(lp), + IsOkAndHolds(EqualsProto(TestLpProto(maximize)))); +} + +TEST_P(ConvertQpMpModelProtoTest, LpRoundTrip) { + const bool maximize = GetParam(); + ASSERT_OK_AND_ASSIGN(QuadraticProgram qp, + QpFromMpModelProto(TestLpProto(maximize), + /*relax_integer_variables=*/false)); + EXPECT_THAT(QpToMpModelProto(qp), + IsOkAndHolds(EqualsProto(TestLpProto(maximize)))); +} + // The QP: // optimize x_0^2 + x_1^2 + 3 x_0 - 4 s.t. // x_0 + x_1 <= 42 @@ -310,6 +333,29 @@ TEST(CanFitInMpModelProto, SmallQpOk) { EXPECT_TRUE(CanFitInMpModelProto(*qp).ok()); } +TEST(CanFitInMpModelProto, TooManyVariablesFails) { + QuadraticProgram qp(1024, 5); + EXPECT_THAT(internal::TestableCanFitInMpModelProto(qp, 1023), + testing::status::StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable"))); +} + +TEST(CanFitInMpModelProto, TooManyConstraintsFails) { + QuadraticProgram qp(3, 1024); + EXPECT_THAT(internal::TestableCanFitInMpModelProto(qp, 1023), + testing::status::StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("constraint"))); +} + +TEST_P(ConvertQpMpModelProtoTest, QpRoundTrip) { + const bool maximize = GetParam(); + ASSERT_OK_AND_ASSIGN(QuadraticProgram qp, + QpFromMpModelProto(TestQpProto(maximize), + /*relax_integer_variables=*/false)); + EXPECT_THAT(QpToMpModelProto(qp), + IsOkAndHolds(EqualsProto(TestQpProto(maximize)))); +} + // The ILP: // optimize x_0 + 2 * x_1 s.t. // x_0 + x_1 <= 1 diff --git a/ortools/pdlp/samples/BUILD.bazel b/ortools/pdlp/samples/BUILD.bazel index 73d1a46e0e..709cdc867a 100644 --- a/ortools/pdlp/samples/BUILD.bazel +++ b/ortools/pdlp/samples/BUILD.bazel @@ -11,6 +11,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -load(":code_samples.bzl", "code_sample_cc") +load("@pip_deps//:requirements.bzl", "requirement") +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_python//python:py_binary.bzl", "py_binary") -code_sample_cc(name = "simple_pdlp_program") +cc_binary( + name = "simple_pdlp_program_cc", + srcs = ["simple_pdlp_program.cc"], + deps = [ + "//ortools/base", + "//ortools/pdlp:iteration_stats", + "//ortools/pdlp:primal_dual_hybrid_gradient", + "//ortools/pdlp:quadratic_program", + "//ortools/pdlp:solve_log_cc_proto", + "//ortools/pdlp:solvers_cc_proto", + "@eigen", + ], +) + +py_binary( + name = "simple_pdlp_program_py", + srcs = ["simple_pdlp_program.py"], + main = "simple_pdlp_program.py", + deps = [ + "//ortools/pdlp:solve_log_py_pb2", + "//ortools/pdlp:solvers_py_pb2", + "//ortools/pdlp/python:pdlp", + requirement("numpy"), + requirement("scipy"), + ], +) diff --git a/ortools/pdlp/samples/code_samples.bzl b/ortools/pdlp/samples/code_samples.bzl deleted file mode 100644 index 6cf9078b8f..0000000000 --- a/ortools/pdlp/samples/code_samples.bzl +++ /dev/null @@ -1,48 +0,0 @@ -# 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. - -"""Helper macro to compile and test code samples.""" - -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -def code_sample_cc(name): - cc_binary( - name = name + "_cc", - srcs = [name + ".cc"], - deps = [ - "//ortools/base", - "//ortools/pdlp:iteration_stats", - "//ortools/pdlp:primal_dual_hybrid_gradient", - "//ortools/pdlp:quadratic_program", - "//ortools/pdlp:solve_log_cc_proto", - "//ortools/pdlp:solvers_cc_proto", - Label("@eigen"), - ], - ) - - cc_test( - name = name + "_cc_test", - size = "small", - srcs = [name + ".cc"], - deps = [ - ":" + name + "_cc", - "//ortools/base", - "//ortools/pdlp:iteration_stats", - "//ortools/pdlp:primal_dual_hybrid_gradient", - "//ortools/pdlp:quadratic_program", - "//ortools/pdlp:solve_log_cc_proto", - "//ortools/pdlp:solvers_cc_proto", - Label("@eigen"), - ], - ) diff --git a/ortools/pdlp/scheduler.h b/ortools/pdlp/scheduler.h index bd908f2e93..9667c99417 100644 --- a/ortools/pdlp/scheduler.h +++ b/ortools/pdlp/scheduler.h @@ -27,7 +27,6 @@ #include #include "absl/functional/any_invocable.h" -#include "absl/log/log.h" #include "absl/synchronization/blocking_counter.h" #include "ortools/base/threadpool.h" #include "ortools/pdlp/solvers.pb.h" diff --git a/ortools/pdlp/solvers_proto_validation_test.cc b/ortools/pdlp/solvers_proto_validation_test.cc index 80cd15b218..fa0cef9be0 100644 --- a/ortools/pdlp/solvers_proto_validation_test.cc +++ b/ortools/pdlp/solvers_proto_validation_test.cc @@ -21,13 +21,13 @@ #include "absl/strings/string_view.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/protobuf_util.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/pdlp/solvers.pb.h" namespace operations_research::pdlp { namespace { -using ::google::protobuf::util::ParseTextOrDie; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; using ::testing::HasSubstr; @@ -49,8 +49,7 @@ void TestTerminationCriteriaValidation( absl::string_view termination_criteria_string, absl::string_view error_substring) { TerminationCriteria termination_criteria = - ParseTextOrDie( - std::string(termination_criteria_string)); + ParseTextOrDie(termination_criteria_string); const absl::Status status = ValidateTerminationCriteria(termination_criteria); EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument) << "With termination criteria \"" << termination_criteria_string << "\""; diff --git a/ortools/pdlp/termination_test.cc b/ortools/pdlp/termination_test.cc index bf53f5dd34..50c049bea8 100644 --- a/ortools/pdlp/termination_test.cc +++ b/ortools/pdlp/termination_test.cc @@ -20,44 +20,16 @@ #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/protobuf_util.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/pdlp/solve_log.pb.h" #include "ortools/pdlp/solvers.pb.h" namespace operations_research::pdlp { -bool operator==(const TerminationCriteria::DetailedOptimalityCriteria& lhs, - const TerminationCriteria::DetailedOptimalityCriteria& rhs) { - if (lhs.eps_optimal_primal_residual_absolute() != - rhs.eps_optimal_primal_residual_absolute()) { - return false; - } - if (lhs.eps_optimal_primal_residual_relative() != - rhs.eps_optimal_primal_residual_relative()) { - return false; - } - if (lhs.eps_optimal_dual_residual_absolute() != - rhs.eps_optimal_dual_residual_absolute()) { - return false; - } - if (lhs.eps_optimal_dual_residual_relative() != - rhs.eps_optimal_dual_residual_relative()) { - return false; - } - if (lhs.eps_optimal_objective_gap_absolute() != - rhs.eps_optimal_objective_gap_absolute()) { - return false; - } - if (lhs.eps_optimal_objective_gap_relative() != - rhs.eps_optimal_objective_gap_relative()) { - return false; - } - return true; -} namespace { -using ::google::protobuf::util::ParseTextOrDie; -using ::testing::Eq; +using ::google::protobuf::contrib::parse_proto::ParseTextOrDie; +using ::testing::EqualsProto; using ::testing::FieldsAre; using ::testing::Optional; @@ -150,17 +122,16 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaOverload) { const auto criteria = ParseTextOrDie( R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb"); - EXPECT_THAT( - EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie( - R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT(EffectiveOptimalityCriteria(criteria), + EqualsProto( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb")); } TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) { @@ -169,17 +140,16 @@ TEST(EffectiveOptimalityCriteriaTest, SimpleOptimalityCriteriaInput) { eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4 })pb"); - EXPECT_THAT( - EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie( - R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT(EffectiveOptimalityCriteria(criteria), + EqualsProto( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb")); } TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) { @@ -193,23 +163,22 @@ TEST(EffectiveOptimalityCriteriaTest, DetailedOptimalityCriteriaInput) { eps_optimal_objective_gap_relative: 6.0e-4 })pb"); EXPECT_THAT(EffectiveOptimalityCriteria(criteria), - Eq(criteria.detailed_optimality_criteria())); + EqualsProto(criteria.detailed_optimality_criteria())); } TEST(EffectiveOptimalityCriteriaTest, DeprecatedInput) { const auto criteria = ParseTextOrDie( R"pb(eps_optimal_absolute: 1.0e-4 eps_optimal_relative: 2.0e-4)pb"); - EXPECT_THAT( - EffectiveOptimalityCriteria(criteria), - Eq(ParseTextOrDie( - R"pb( - eps_optimal_primal_residual_absolute: 1.0e-4 - eps_optimal_primal_residual_relative: 2.0e-4 - eps_optimal_dual_residual_absolute: 1.0e-4 - eps_optimal_dual_residual_relative: 2.0e-4 - eps_optimal_objective_gap_absolute: 1.0e-4 - eps_optimal_objective_gap_relative: 2.0e-4 - )pb"))); + EXPECT_THAT(EffectiveOptimalityCriteria(criteria), + EqualsProto( + R"pb( + eps_optimal_primal_residual_absolute: 1.0e-4 + eps_optimal_primal_residual_relative: 2.0e-4 + eps_optimal_dual_residual_absolute: 1.0e-4 + eps_optimal_dual_residual_relative: 2.0e-4 + eps_optimal_objective_gap_absolute: 1.0e-4 + eps_optimal_objective_gap_relative: 2.0e-4 + )pb")); } TEST_P(DetailedRelativeTerminationTest, TerminationWithNearOptimal) { diff --git a/ortools/routing/parsers/testdata/BUILD.bazel b/ortools/routing/parsers/testdata/BUILD.bazel index 261a7f6c18..164b8ce156 100644 --- a/ortools/routing/parsers/testdata/BUILD.bazel +++ b/ortools/routing/parsers/testdata/BUILD.bazel @@ -11,23 +11,28 @@ # See the License for the specific language governing permissions and # limitations under the License. +package(default_visibility = ["//visibility:public"]) + exports_files([ "c1_10_2-90-42222.96.txt", + "C1_10_2.TXT", "carp_gdb19.dat", # https://github.com/vidalt/HGS-CARP/blob/master/Instances/CARP/gdb19.dat # https://www.sciencedirect.com/science/article/abs/pii/0305054883900266 "carp_gdb19_diferente_deposito.dat", - "carp_gdb19_incorrecto_vertices.dat", "carp_gdb19_incorrecta_lista_aristas_req.dat", "carp_gdb19_incorrecto_arinoreq.dat", "carp_gdb19_incorrecto_arireq.dat", + "carp_gdb19_incorrecto_arista.dat", "carp_gdb19_incorrecto_capacidad.dat", "carp_gdb19_incorrecto_coste.dat", "carp_gdb19_incorrecto_deposito.dat", "carp_gdb19_incorrecto_tipo.dat", "carp_gdb19_incorrecto_vehiculos.dat", + "carp_gdb19_incorrecto_vertices.dat", "carp_gdb19_mixed_arcs.dat", "carp_gdb19_no_arista_req.dat", "carp_toy.dat", + "carp_toy.sol", "lilim.zip", "n20w20.001.txt", "n20w20.002.txt", @@ -39,6 +44,7 @@ exports_files([ "rc201.0", "solomon_bp_c101.mps", "solomon_bp_c101.pb", + "solomon_check_id.md", "solomon_check_id.txt", "solomon_google2.delivery_at_depot_location.pb.txt", "solomon_google2.delivery_only.pb.txt", @@ -48,5 +54,6 @@ exports_files([ "solomon.zip", "tsplib_ar9152.tour", "tsplib_ar9152.tsp", + "tsplib_F-n45-k4.vrp", "tsplib_Kytojoki_33.vrp", ]) diff --git a/ortools/util/python/BUILD.bazel b/ortools/util/python/BUILD.bazel index 1f78f70ae7..c07a1d2df8 100644 --- a/ortools/util/python/BUILD.bazel +++ b/ortools/util/python/BUILD.bazel @@ -81,9 +81,7 @@ pybind_extension( srcs = ["pybind_solve_interrupter.cc"], # This library is not meant to be consumed end users; only pybind11 code # that needs a SolverInterrupter. - visibility = [ - "//ortools/math_opt/core/python:__subpackages__", - ], + visibility = ["//visibility:public"], deps = [":py_solve_interrupter_lib"], ) @@ -91,6 +89,7 @@ pybind_extension( name = "pybind_solve_interrupter_testing", testonly = True, srcs = ["pybind_solve_interrupter_testing.cc"], + visibility = ["//visibility:public"], deps = [":py_solve_interrupter_testing_lib"], )