diff --git a/cmake/python.cmake b/cmake/python.cmake index d57054e5254..9cdd8ba8c57 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -420,6 +420,7 @@ file(COPY DESTINATION ${PYTHON_PROJECT_DIR}/sat/colab) file(COPY ortools/util/python/solve_interrupter.py + ortools/util/python/status_streaming.py DESTINATION ${PYTHON_PROJECT_DIR}/util/python) # Adds py.typed to make typed packages. diff --git a/ortools/java/com/google/ortools/mathopt/SolverType.java b/ortools/java/com/google/ortools/mathopt/SolverType.java index bc5672a145f..0c8520cb668 100644 --- a/ortools/java/com/google/ortools/mathopt/SolverType.java +++ b/ortools/java/com/google/ortools/mathopt/SolverType.java @@ -116,7 +116,28 @@ public enum SolverType { * *

Supports LP, MIP, and nonconvex integer quadratic problems. Need a special license. */ - XPRESS(SolverTypeProto.SOLVER_TYPE_XPRESS); + XPRESS(SolverTypeProto.SOLVER_TYPE_XPRESS), + + /** + * Google's Min-Cost Flow solver. + * + *

Uses a specialized solver for Min-Cost Flow problems (see + * https://developers.google.com/optimization/flow/mincostflow). Supports LP problems that match + * the structure of a Min-Cost Flow problem (see go/mathopt-min-cost-flow). + * + *

Requirements: + * + *

+ */ + MIN_COST_FLOW(SolverTypeProto.SOLVER_TYPE_MIN_COST_FLOW); private static class ProtoMap { private static final EnumMap map = diff --git a/ortools/javatests/com/google/ortools/mathopt/SolveTest.java b/ortools/javatests/com/google/ortools/mathopt/SolveTest.java index 5b5f91f0153..de2dad62f90 100644 --- a/ortools/javatests/com/google/ortools/mathopt/SolveTest.java +++ b/ortools/javatests/com/google/ortools/mathopt/SolveTest.java @@ -361,7 +361,6 @@ public void indicatorConstraintWithNonIntegerIndicator_throws() { model.addLeIndicatorConstraint(z, false, Expressions.linExpr(x), 5.0, "c"); // Expect error during solve. - assertThrows(IllegalArgumentException.class, - () -> { SolveResult unused = Solve.solve(model, SolverType.GSCIP); }); + assertThrows(IllegalArgumentException.class, () -> Solve.solve(model, SolverType.GSCIP)); } } diff --git a/ortools/math_opt/BUILD.bazel b/ortools/math_opt/BUILD.bazel index 1f8df9fcc8e..d35358a68c8 100644 --- a/ortools/math_opt/BUILD.bazel +++ b/ortools/math_opt/BUILD.bazel @@ -268,6 +268,7 @@ proto_library( ":model_update_proto", ":parameters_proto", ":result_proto", + "//ortools/util:status_proto", ], ) diff --git a/ortools/math_opt/constraints/quadratic/BUILD.bazel b/ortools/math_opt/constraints/quadratic/BUILD.bazel index e5bf1dd8da0..6b65d52c48d 100644 --- a/ortools/math_opt/constraints/quadratic/BUILD.bazel +++ b/ortools/math_opt/constraints/quadratic/BUILD.bazel @@ -43,6 +43,7 @@ cc_test( "//ortools/base:gmock", "//ortools/base:gmock_main", "//ortools/base:strong_int", + "//ortools/math_opt/constraints/util:model_util", "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/storage:model_storage", "//ortools/math_opt/storage:sparse_coefficient_map", diff --git a/ortools/math_opt/constraints/quadratic/quadratic_constraint.cc b/ortools/math_opt/constraints/quadratic/quadratic_constraint.cc index f75f42c53c9..dbd982c6529 100644 --- a/ortools/math_opt/constraints/quadratic/quadratic_constraint.cc +++ b/ortools/math_opt/constraints/quadratic/quadratic_constraint.cc @@ -15,7 +15,6 @@ #include -#include "ortools/base/strong_int.h" #include "ortools/math_opt/cpp/variable_and_expressions.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/math_opt/storage/sparse_coefficient_map.h" diff --git a/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc b/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc index d29eb095094..2eb30287962 100644 --- a/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc +++ b/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc @@ -19,7 +19,7 @@ #include "absl/strings/str_cat.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/strong_int.h" +#include "ortools/math_opt/constraints/util/model_util.h" #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/storage/model_storage.h" #include "ortools/math_opt/storage/sparse_coefficient_map.h" diff --git a/ortools/math_opt/constraints/quadratic/storage_test.cc b/ortools/math_opt/constraints/quadratic/storage_test.cc index 170dfdf58ff..75c0b35dccb 100644 --- a/ortools/math_opt/constraints/quadratic/storage_test.cc +++ b/ortools/math_opt/constraints/quadratic/storage_test.cc @@ -17,7 +17,6 @@ #include "gtest/gtest.h" #include "ortools/base/gmock.h" -#include "ortools/base/strong_int.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/storage/model_storage_types.h" diff --git a/ortools/math_opt/constraints/quadratic/validator_test.cc b/ortools/math_opt/constraints/quadratic/validator_test.cc index f5e21527fac..136f06a181e 100644 --- a/ortools/math_opt/constraints/quadratic/validator_test.cc +++ b/ortools/math_opt/constraints/quadratic/validator_test.cc @@ -16,7 +16,6 @@ #include #include #include -#include #include "absl/log/check.h" #include "absl/status/status.h" diff --git a/ortools/math_opt/core/BUILD.bazel b/ortools/math_opt/core/BUILD.bazel index 78d2d1228cb..4a2d6fcfdd3 100644 --- a/ortools/math_opt/core/BUILD.bazel +++ b/ortools/math_opt/core/BUILD.bazel @@ -291,6 +291,8 @@ cc_test( deps = [ ":non_streamable_solver_init_arguments", "//ortools/base:gmock_main", + "//ortools/math_opt:parameters_cc_proto", + "@abseil-cpp//absl/base:core_headers", ], ) @@ -346,6 +348,7 @@ cc_library( hdrs = ["solver_interface_testing.h"], deps = [ ":solver_interface", + "//ortools/math_opt:parameters_cc_proto", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", @@ -359,6 +362,8 @@ cc_test( ":solver_interface", ":solver_interface_testing", "//ortools/base:gmock", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:parameters_cc_proto", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", @@ -453,6 +458,7 @@ cc_test( ":empty_bounds", ":model_summary", "//ortools/base:gmock_main", + "//ortools/math_opt:result_cc_proto", "//ortools/math_opt/validators:result_validator", ], ) diff --git a/ortools/math_opt/core/empty_bounds.cc b/ortools/math_opt/core/empty_bounds.cc index 0c8dfe1cfbc..89d6adcd581 100644 --- a/ortools/math_opt/core/empty_bounds.cc +++ b/ortools/math_opt/core/empty_bounds.cc @@ -13,6 +13,7 @@ #include "ortools/math_opt/core/empty_bounds.h" +#include #include #include "absl/strings/str_cat.h" diff --git a/ortools/math_opt/core/empty_bounds_test.cc b/ortools/math_opt/core/empty_bounds_test.cc index 9f65a1cc082..88108c0f01f 100644 --- a/ortools/math_opt/core/empty_bounds_test.cc +++ b/ortools/math_opt/core/empty_bounds_test.cc @@ -16,6 +16,7 @@ #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/result.pb.h" #include "ortools/math_opt/validators/result_validator.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/core/inverted_bounds_test.cc b/ortools/math_opt/core/inverted_bounds_test.cc index cdf7e4b9188..116d886449d 100644 --- a/ortools/math_opt/core/inverted_bounds_test.cc +++ b/ortools/math_opt/core/inverted_bounds_test.cc @@ -14,7 +14,6 @@ #include "ortools/math_opt/core/inverted_bounds.h" #include -#include #include #include diff --git a/ortools/math_opt/core/model_summary_test.cc b/ortools/math_opt/core/model_summary_test.cc index e7b26ff10bc..2aba737ced8 100644 --- a/ortools/math_opt/core/model_summary_test.cc +++ b/ortools/math_opt/core/model_summary_test.cc @@ -14,7 +14,6 @@ #include "ortools/math_opt/core/model_summary.h" #include -#include #include #include #include diff --git a/ortools/math_opt/core/non_streamable_solver_init_arguments_test.cc b/ortools/math_opt/core/non_streamable_solver_init_arguments_test.cc index fcb8775ffd9..30f25b53bcd 100644 --- a/ortools/math_opt/core/non_streamable_solver_init_arguments_test.cc +++ b/ortools/math_opt/core/non_streamable_solver_init_arguments_test.cc @@ -16,8 +16,10 @@ #include #include +#include "absl/base/attributes.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" +#include "ortools/math_opt/parameters.pb.h" namespace operations_research::math_opt { namespace { diff --git a/ortools/math_opt/core/solver_interface_testing.h b/ortools/math_opt/core/solver_interface_testing.h index c78b85a7644..eef6107ac52 100644 --- a/ortools/math_opt/core/solver_interface_testing.h +++ b/ortools/math_opt/core/solver_interface_testing.h @@ -18,6 +18,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/parameters.pb.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/core/solver_interface_testing_test.cc b/ortools/math_opt/core/solver_interface_testing_test.cc index df75c97a705..23880ed8a64 100644 --- a/ortools/math_opt/core/solver_interface_testing_test.cc +++ b/ortools/math_opt/core/solver_interface_testing_test.cc @@ -21,6 +21,8 @@ #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/parameters.pb.h" namespace operations_research::math_opt { namespace { diff --git a/ortools/math_opt/core/sparse_submatrix_test.cc b/ortools/math_opt/core/sparse_submatrix_test.cc index fe6d6a09ccc..c6cc2255610 100644 --- a/ortools/math_opt/core/sparse_submatrix_test.cc +++ b/ortools/math_opt/core/sparse_submatrix_test.cc @@ -15,8 +15,6 @@ #include #include -#include -#include #include #include diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index ff721cc8d55..c6d397201fd 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -231,6 +231,7 @@ cc_test( "no_test_android", "no_test_darwin_x86_64", "no_test_wasm", + "noci", ], deps = [ ":formatters", diff --git a/ortools/math_opt/cpp/executor/BUILD.bazel b/ortools/math_opt/cpp/executor/BUILD.bazel new file mode 100644 index 00000000000..119e63082eb --- /dev/null +++ b/ortools/math_opt/cpp/executor/BUILD.bazel @@ -0,0 +1,123 @@ +# 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. + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "executor_init_args", + hdrs = ["executor_init_args.h"], + deps = [ + "//ortools/math_opt/cpp:math_opt", + "//ortools/util:solve_interrupter", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/time", + ], +) + +cc_library( + name = "solve_executor", + hdrs = ["solve_executor.h"], + deps = [ + ":executor_init_args", + "//ortools/math_opt/cpp:math_opt", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_library( + name = "time_limit_util", + srcs = ["time_limit_util.cc"], + hdrs = ["time_limit_util.h"], + deps = [ + "//ortools/math_opt/cpp:math_opt", + "@abseil-cpp//absl/time", + ], +) + +cc_test( + name = "time_limit_util_test", + srcs = ["time_limit_util_test.cc"], + deps = [ + ":time_limit_util", + "//ortools/base:gmock_main", + "//ortools/math_opt/cpp:math_opt", + "@abseil-cpp//absl/time", + ], +) + +cc_library( + name = "local_solve_executor", + srcs = ["local_solve_executor.cc"], + hdrs = ["local_solve_executor.h"], + deps = [ + ":executor_init_args", + ":solve_executor", + ":time_limit_util", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/util:solve_interrupter", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/time", + ], +) + +cc_test( + name = "local_solve_executor_test", + srcs = ["local_solve_executor_test.cc"], + deps = [ + ":local_solve_executor", + ":solve_executor_tests", + "//ortools/base:gmock_main", + "//ortools/math_opt/solvers:glop_solver", + "//ortools/math_opt/solvers:gscip_solver", + ], +) + +cc_test( + name = "local_solve_executor_gurobi_test", + srcs = ["local_solve_executor_gurobi_test.cc"], + tags = [ + "local", + "manual", + "not_build:arm", + "not_run:arm", + ], + deps = [ + ":local_solve_executor", + ":solve_executor_gurobi_tests", + "//ortools/base:gmock_main", + ], +) + +cc_library( + name = "solve_executor_tests", + testonly = True, + srcs = ["solve_executor_tests.cc"], + hdrs = ["solve_executor_tests.h"], + deps = [ + ":solve_executor", + "//ortools/base:gmock", + "//ortools/math_opt/cpp:matchers", + "//ortools/math_opt/cpp:math_opt", + "//ortools/port:scoped_std_stream_capture", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/time", + ], +) diff --git a/ortools/math_opt/cpp/executor/README.md b/ortools/math_opt/cpp/executor/README.md new file mode 100644 index 00000000000..7b80a9bf693 --- /dev/null +++ b/ortools/math_opt/cpp/executor/README.md @@ -0,0 +1,26 @@ +# Solve Executor + +The **Solve Executor** library is an extension of the MathOpt C++ API designed +to make different execution modes interoperable without imposing compile-time +dependencies on specific modes. + +## Overview + +The API mirrors the standard MathOpt in-process solving API. Instead of directly +calling `Solve(Model, SolverType, SolveArguments, SolverInitArguments)`, you: +1. Create a `SolveExecutor`. +2. Call `SolveExecutor::Solve(Model, SolverType, SolveArguments, ExecutorSolverInitArguments)`. + +This virtual interface allows different implementations to execute solves +differently. + +## Implementations + +The open-source version includes: + +* `LocalSolveExecutor`: Solves the model in-process using the standard `Solve()` function. + +## Incremental Solving + +You can also create an `ExecutorIncrementalSolver` (similar to +`IncrementalSolver`) for cases where you need to solve a model incrementally. diff --git a/ortools/math_opt/cpp/executor/executor_init_args.h b/ortools/math_opt/cpp/executor/executor_init_args.h new file mode 100644 index 00000000000..d14c98c1d09 --- /dev/null +++ b/ortools/math_opt/cpp/executor/executor_init_args.h @@ -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. + +#ifndef ORTOOLS_MATH_OPT_CPP_EXECUTOR_EXECUTOR_INIT_ARGS_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_EXECUTOR_INIT_ARGS_H_ + +#include + +#include "absl/base/nullability.h" +#include "absl/time/time.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research::math_opt { + +// Similar to SolverInitArguments, but for the executor APIs. +struct ExecutorSolverInitArguments { + // An explicit deadline for the RPC call. + // + // For stubby users, see remote_solve.h for a complete explanation on how this + // interacts with any propagated deadline from base::Context. + std::optional explicit_deadline; + + // Arguments controlling the initialization of the solver. + math_opt::StreamableSolverInitArguments streamable; + + // End the solve as soon as possible. In contrast to interrupter, + // absl::Cancelled error might be returned. For incremental solver, may be in + // an unusable state after. + const SolveInterrupter* absl_nullable canceller = nullptr; + + // If true, the names of variables and constraints are discarded before + // sending them to the solver. This is particularly useful for models near the + // two gigabyte limit in proto form (all models solved by remote solve must + // be serialized). + bool remove_names = false; + + // Hints on resources requested for the solve. + math_opt::SolverResources resources; +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_EXECUTOR_INIT_ARGS_H_ diff --git a/ortools/math_opt/cpp/executor/local_solve_executor.cc b/ortools/math_opt/cpp/executor/local_solve_executor.cc new file mode 100644 index 00000000000..31d04e6df2a --- /dev/null +++ b/ortools/math_opt/cpp/executor/local_solve_executor.cc @@ -0,0 +1,115 @@ +// 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/math_opt/cpp/executor/local_solve_executor.h" + +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/statusor.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/executor/executor_init_args.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" +#include "ortools/math_opt/cpp/executor/time_limit_util.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research::math_opt { +namespace { + +// Creates a new interrupter that is interrupted if either of the input +// interrupters is interrupted. +// +// Either input interrupter can be null. When both inputs are null, +// local_interrupter_if_nonempty() will return null instead of an interrupter. +class ChainedInterrupter { + public: + ChainedInterrupter(const SolveInterrupter* absl_nullable i1, + const SolveInterrupter* absl_nullable i2) + : from_i1_(i1, [this]() { local_interrupter_.Interrupt(); }), + from_i2_(i2, [this]() { local_interrupter_.Interrupt(); }), + empty_(i1 == nullptr && i2 == nullptr) {} + + bool empty() const { return empty_; } + const SolveInterrupter* absl_nonnull local_interrupter() const { + return &local_interrupter_; + } + const SolveInterrupter* absl_nullable local_interrupter_if_nonempty() const { + return empty_ ? nullptr : &local_interrupter_; + } + + private: + SolveInterrupter local_interrupter_; + ScopedSolveInterrupterCallback from_i1_; + ScopedSolveInterrupterCallback from_i2_; + const bool empty_; +}; + +} // namespace + +absl::StatusOr LocalIncrementalSolver::Solve(SolveArguments args) { + UpdateTimeLimit(absolute_deadline_, args.parameters); + const ChainedInterrupter chained_interrupter(canceller_, args.interrupter); + args.interrupter = chained_interrupter.local_interrupter_if_nonempty(); + return solver_->Solve(args); +} + +absl::StatusOr LocalSolveExecutor::Solve( + const Model& model, SolverType solver_type, SolveArguments args, + ExecutorSolverInitArguments init_args) { + UpdateTimeLimit( + init_args.explicit_deadline.value_or(absl::InfiniteDuration()), + args.parameters); + const ChainedInterrupter chained_interrupter(init_args.canceller, + args.interrupter); + args.interrupter = chained_interrupter.local_interrupter_if_nonempty(); + return ::operations_research::math_opt::Solve( + model, solver_type, args, + SolverInitArguments{.streamable = std::move(init_args.streamable), + .remove_names = init_args.remove_names}); +} + +absl::StatusOr +LocalSolveExecutor::ComputeInfeasibleSubsystem( + const Model& model, SolverType solver_type, + ComputeInfeasibleSubsystemArguments args, + ExecutorSolverInitArguments init_args) { + UpdateTimeLimit( + init_args.explicit_deadline.value_or(absl::InfiniteDuration()), + args.parameters); + const ChainedInterrupter chained_interrupter(init_args.canceller, + args.interrupter); + args.interrupter = chained_interrupter.local_interrupter_if_nonempty(); + return ::operations_research::math_opt::ComputeInfeasibleSubsystem( + model, solver_type, args); +} + +absl::StatusOr> +LocalSolveExecutor::New(Model* model, SolverType solver_type, + ExecutorSolverInitArguments init_args) { + absl::Time absolute_deadline = + absl::Now() + + init_args.explicit_deadline.value_or(absl::InfiniteDuration()); + OR_ASSIGN_OR_RETURN( + std::unique_ptr solver, + NewIncrementalSolver(model, solver_type, + {.streamable = std::move(init_args.streamable), + .remove_names = init_args.remove_names})); + return std::make_unique( + std::move(solver), absolute_deadline, init_args.canceller); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/local_solve_executor.h b/ortools/math_opt/cpp/executor/local_solve_executor.h new file mode 100644 index 00000000000..88322343678 --- /dev/null +++ b/ortools/math_opt/cpp/executor/local_solve_executor.h @@ -0,0 +1,91 @@ +// 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_MATH_OPT_CPP_EXECUTOR_LOCAL_SOLVE_EXECUTOR_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_LOCAL_SOLVE_EXECUTOR_H_ + +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "ortools/math_opt/cpp/executor/executor_init_args.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +// An ExecutorIncrementalSolver that runs in process on the same thread. +// +// Create with LocalSolveExecutor::New(). +// +// This class is a thin wrapper around IncrementalSolver, with the same +// modifications on deadline and cancellation as LocalSolveExecutor. +class LocalIncrementalSolver : public ExecutorIncrementalSolver { + public: + using ExecutorIncrementalSolver::Solve; + absl::StatusOr Solve(SolveArguments args) override; + + // Access via LocalSolveExecutor::New(). + explicit LocalIncrementalSolver( + std::unique_ptr solver, + const absl::Time absolute_deadline, + const SolveInterrupter* absl_nullable const canceller) + : solver_(std::move(solver)), + absolute_deadline_(absolute_deadline), + canceller_(canceller) {} + + SolverType solver_type() const override { return solver_->solver_type(); } + + private: + std::unique_ptr solver_; + absl::Time absolute_deadline_; + const SolveInterrupter* absl_nullable canceller_; +}; + +// A `SolveExecutor` where all solves run in process and are started from the +// same thread (some solvers create more threads). +// +// Features not found on ::operations_research::math_opt::Solve(): +// * If an explicit deadline is provided, the time limit is set to the minimum +// of the existing time limit and the deadline. +// * If a canceller is provided, we interrupt if the canceller is triggered. +// +// This class is stateless, copyable, and movable. It is threadsafe to call the +// functions on this class concurrently. This class is not aware of fiber +// cancellations or base context deadlines. +class LocalSolveExecutor : public SolveExecutor { + public: + LocalSolveExecutor() = default; + + using SolveExecutor::Solve; + absl::StatusOr Solve( + const Model& model, SolverType solver_type, SolveArguments args, + ExecutorSolverInitArguments init_args) override; + + using SolveExecutor::ComputeInfeasibleSubsystem; + absl::StatusOr ComputeInfeasibleSubsystem( + const Model& model, SolverType solver_type, + ComputeInfeasibleSubsystemArguments args, + ExecutorSolverInitArguments init_args) override; + + using SolveExecutor::New; + absl::StatusOr> New( + Model* model, SolverType solver_type, + ExecutorSolverInitArguments init_args) override; +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_LOCAL_SOLVE_EXECUTOR_H_ diff --git a/ortools/math_opt/cpp/executor/local_solve_executor_gurobi_test.cc b/ortools/math_opt/cpp/executor/local_solve_executor_gurobi_test.cc new file mode 100644 index 00000000000..ed13c3502d4 --- /dev/null +++ b/ortools/math_opt/cpp/executor/local_solve_executor_gurobi_test.cc @@ -0,0 +1,31 @@ +// 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 "gtest/gtest.h" +#include "ortools/math_opt/cpp/executor/local_solve_executor.h" +#include "ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.h" + +namespace operations_research::math_opt { +namespace { + +INSTANTIATE_TEST_SUITE_P(LocalSolveExecutorGurobiTests, SolveExecutorGurobiTest, + testing::Values(SolveExecutorGurobiTestParameters{ + .name = "local_solve", .executor_provider = []() { + return std::make_unique(); + }})); + +} // namespace + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/local_solve_executor_test.cc b/ortools/math_opt/cpp/executor/local_solve_executor_test.cc new file mode 100644 index 00000000000..91255059471 --- /dev/null +++ b/ortools/math_opt/cpp/executor/local_solve_executor_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/math_opt/cpp/executor/local_solve_executor.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/math_opt/cpp/executor/solve_executor_tests.h" + +namespace operations_research::math_opt { +namespace { + +INSTANTIATE_TEST_SUITE_P( + LocalSolveExecutorTests, SolveExecutorTest, + testing::Values(SolveExecutorTestParameters{ + .name = "local_solve", + .executor_provider = + []() { return std::make_unique(); }, + .features = {.interruption = true, + .solve_callback = true, + .actual_incrementalism = true}})); + +} // namespace + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/solve_executor.h b/ortools/math_opt/cpp/executor/solve_executor.h new file mode 100644 index 00000000000..c7597962347 --- /dev/null +++ b/ortools/math_opt/cpp/executor/solve_executor.h @@ -0,0 +1,144 @@ +// 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_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_H_ + +#include +#include + +#include "absl/status/statusor.h" +#include "ortools/math_opt/cpp/executor/executor_init_args.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +// An optimization solver that can solve an input `Model` and repeatedly resolve +// the model after modifications. +// +// Create with `SolveExecutor::New()`. +// +// Similar to `IncrementalSolver`, but the underlying solver may run in a +// different process or even a different machine. See docs on IncrementalSolver +// (../solve.h) for more details. +// +// Implementations of this class are generally not threadsafe (do not run +// Solve() concurrently with another Solve() or with any modifications to the +// underlying model). However, the destructor and modifications to the +// underlying Model can run concurrently. +class ExecutorIncrementalSolver { + public: + virtual ~ExecutorIncrementalSolver() = default; + + // Calls Solve(SolveArguments{}), see that function for details. + absl::StatusOr Solve() { return Solve({}); } + + // Solves the Model and returns the result. + // + // Ensure that the underlying model outlives the returned SolveResult. + // Modification of the Model (in particular, deleting variables/constraints) + // can also invalidate the SolveResult. + // + // Generally, any Status error returned indicates that object is in a bad + // state and that subsequent calls to Solve() will also fail. For some + // subclasses solving by RPC, there may be an exception for retrying on + // network errors, the exceptions are documented in each implementation. + virtual absl::StatusOr Solve(SolveArguments args) = 0; + + virtual SolverType solver_type() const = 0; +}; + +// An interface for solving optimization models and creating incremental +// solvers for a model. +// +// Implementations differ on thread-safety, supporting move/copy, being fiber +// aware, and respecting base context deadlines. +class SolveExecutor { + public: + virtual ~SolveExecutor() = default; + + // Solves the input model, similar to the function + // ::operations_research::math_opt::Solve(). + // + // Ensure `model` outlives the returned SolveResult. Deleting variables or + // constraints from `model` may invalidate the returned SolveResult. + virtual absl::StatusOr Solve( + const Model& model, SolverType solver_type, SolveArguments args, + ExecutorSolverInitArguments init_args) = 0; + + // Calls Solve(model, solver_type, args, ExecutorSolverInitArguments{}), see + // overload for details. + absl::StatusOr Solve(const Model& model, + const SolverType solver_type, + SolveArguments args) { + return Solve(model, solver_type, std::move(args), {}); + } + + // Calls Solve(model, solver_type, SolveArguments{}), see overload for + // details. + absl::StatusOr Solve(const Model& model, + const SolverType solver_type) { + return Solve(model, solver_type, {}); + } + + // Checks if a model is infeasible, and computes an infeasible subsystem + // (a subset of the variables/constraints that causes infeasibility) if one + // exists (generally trying to find a small explanation). Similar to + // ::operations_research::math_opt::ComputeInfeasibleSubsystem(). + // + // Ensure `model` outlives the returned ComputeInfeasibleSubsystemResult. + // Deleting variables or constraints from `model` may invalidate the returned + // ComputeInfeasibleSubsystemResult. + virtual absl::StatusOr + ComputeInfeasibleSubsystem(const Model& model, SolverType solver_type, + ComputeInfeasibleSubsystemArguments args, + ExecutorSolverInitArguments init_args) = 0; + + // Calls ComputeInfeasibleSubsystem(model, solver_type, args, + // ExecutorSolverInitArguments{}), see overload for details. + absl::StatusOr ComputeInfeasibleSubsystem( + const Model& model, const SolverType solver_type, + ComputeInfeasibleSubsystemArguments args) { + return ComputeInfeasibleSubsystem(model, solver_type, std::move(args), {}); + } + + // Calls ComputeInfeasibleSubsystem(model, solver_type, + // ComputeInfeasibleSubsystemArguments{}), see overload for details. + absl::StatusOr ComputeInfeasibleSubsystem( + const Model& model, const SolverType solver_type) { + return ComputeInfeasibleSubsystem(model, solver_type, {}); + } + + // Returns a solver that solves `model`, updating for any modifications to + // model between calls to Solve(). + // + // Requires that `model` outlive the returned ExecutorIncrementalSolver. + // + // The underlying solver and SolverExecutor generally try to make sequential + // calls to solve more efficient than solving from scratch. The details are + // different for each implementation and in some cases there is no gain. + virtual absl::StatusOr> New( + Model* model, SolverType solver_type, + ExecutorSolverInitArguments init_args) = 0; + + // Calls New(model, solver_type, ExecutorSolverInitArguments{}), see overload + // for details. + absl::StatusOr> New( + Model* model, const SolverType solver_type) { + return New(model, solver_type, {}); + } +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_H_ diff --git a/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.cc b/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.cc new file mode 100644 index 00000000000..2103800f67b --- /dev/null +++ b/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.cc @@ -0,0 +1,96 @@ +// 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/math_opt/cpp/executor/solve_executor_gurobi_tests.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/gurobi/gurobi_testing.h" +#include "ortools/math_opt/cpp/executor/executor_init_args.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/util/testing_utils.h" + +namespace operations_research::math_opt { + +namespace { + +using ::testing::status::IsOkAndHolds; + +} // namespace + +std::ostream& operator<<(std::ostream& ostr, + const SolveExecutorGurobiTestParameters& params) { + ostr << params.name; + return ostr; +} + +math_opt::ExecutorSolverInitArguments +SolveExecutorGurobiTestParameters::init_args() const { + math_opt::ExecutorSolverInitArguments result; + if (isv_key.has_value()) { + result.streamable.gurobi = math_opt::StreamableGurobiInitArguments{ + .isv_key = math_opt::GurobiISVKey::FromProto(*isv_key), + }; + } + return result; +} + +void RunSolveSuccess(const SolveExecutorGurobiTestParameters& params) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + std::unique_ptr executor = params.executor_provider(); + EXPECT_THAT( + executor->Solve(model, SolverType::kGurobi, {}, params.init_args()), + IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}}))); +} + +void RunIISSuccess(const SolveExecutorGurobiTestParameters& params) { + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + const Variable z = model.AddBinaryVariable("z"); + model.AddLinearConstraint(x + y <= 1); + model.AddLinearConstraint(y + z <= 1); + model.AddLinearConstraint(x + z <= 1); + model.AddLinearConstraint(x + y + z >= 2); + model.Maximize(x); + std::unique_ptr executor = params.executor_provider(); + EXPECT_THAT(executor->ComputeInfeasibleSubsystem(model, SolverType::kGurobi, + {}, params.init_args()), + IsOkAndHolds(IsInfeasible())); +} + +namespace { + +TEST_P(SolveExecutorGurobiTest, SolveSuccess) { + if (kAnyXsanEnabled) { + GTEST_SKIP() << kGurobiFailsWithXSAN; + } + RunSolveSuccess(GetParam()); +} + +TEST_P(SolveExecutorGurobiTest, IISSuccess) { + if (kAnyXsanEnabled) { + GTEST_SKIP() << kGurobiFailsWithXSAN; + } + RunIISSuccess(GetParam()); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.h b/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.h new file mode 100644 index 00000000000..34e1cd12592 --- /dev/null +++ b/ortools/math_opt/cpp/executor/solve_executor_gurobi_tests.h @@ -0,0 +1,57 @@ +// 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_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_GUROBI_TESTS_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_GUROBI_TESTS_H_ + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/executor/executor_init_args.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" + +namespace operations_research::math_opt { + +struct SolveExecutorGurobiTestParameters { + std::string name; + std::function()> executor_provider; + std::optional isv_key; + + ExecutorSolverInitArguments init_args() const; + std::function setup = []() { return absl::OkStatus(); }; +}; + +std::ostream& operator<<(std::ostream& ostr, + const SolveExecutorGurobiTestParameters& params); + +class SolveExecutorGurobiTest + : public testing::TestWithParam { + public: + std::unique_ptr MakeExecutor() { + return GetParam().executor_provider(); + } + void SetUp() override { ASSERT_OK(GetParam().setup()); } +}; + +// TODO(b/347046083): delete these functions once we can use UOSS with ITS. +void RunSolveSuccess(const SolveExecutorGurobiTestParameters& params); +void RunIISSuccess(const SolveExecutorGurobiTestParameters& params); + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_GUROBI_TESTS_H_ diff --git a/ortools/math_opt/cpp/executor/solve_executor_tests.cc b/ortools/math_opt/cpp/executor/solve_executor_tests.cc new file mode 100644 index 00000000000..6cfa72951c8 --- /dev/null +++ b/ortools/math_opt/cpp/executor/solve_executor_tests.cc @@ -0,0 +1,308 @@ +// 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/math_opt/cpp/executor/solve_executor_tests.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_join.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/port/scoped_std_stream_capture.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::AnyOf; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +// CanonicalStatusIs is only available internally +template +inline auto CanonicalStatusIs(Args&&... args) { + return StatusIs(std::forward(args)...); +} +} // namespace + +std::ostream& operator<<(std::ostream& ostr, + const SolveExecutorTestParameters& params) { + ostr << params.name; + return ostr; +} + +namespace { + +TEST_P(SolveExecutorTest, SolveSuccess) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + EXPECT_THAT(executor->Solve(model, SolverType::kGlop), + IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}}))); +} + +TEST_P(SolveExecutorTest, CancelBeforeStart) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + SolveInterrupter canceller; + canceller.Interrupt(); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + EXPECT_THAT( + executor->Solve(model, SolverType::kGlop, {}, {.canceller = &canceller}), + AnyOf(CanonicalStatusIs(absl::StatusCode::kCancelled), + IsOkAndHolds(TerminatesWithLimit(Limit::kInterrupted)))); +} + +TEST_P(SolveExecutorTest, InterruptBeforeStart) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + if (GetParam().name == "subprocess_solve") { + GTEST_SKIP() << "Subprocess solve does not guarantee interruptions before " + "the start of a solve are respected, see b/346799566"; + } + SolveInterrupter interrupter; + interrupter.Interrupt(); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + const absl::StatusOr result = + executor->Solve(model, SolverType::kGlop, {.interrupter = &interrupter}); + if (features().interruption) { + EXPECT_THAT(result, IsOkAndHolds(TerminatesWithLimit(Limit::kInterrupted))); + } else { + EXPECT_THAT(result, IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}}))); + } +} + +TEST_P(SolveExecutorTest, SolveCallback) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddBinaryVariable("y"); + model.Maximize(2 * x + y); + auto cb = [x, y](const CallbackData& data) { + CHECK_EQ(data.event, CallbackEvent::kMipSolution); + CallbackResult result; + if (data.solution->at(x) + data.solution->at(y) >= 1.0 + 1e-5) { + result.AddLazyConstraint(x + y <= 1.0); + } + return result; + }; + const absl::StatusOr result = executor->Solve( + model, SolverType::kGscip, + {.callback_registration = {.events = {CallbackEvent::kMipSolution}, + .add_lazy_constraints = true}, + .callback = std::move(cb)}); + if (features().solve_callback) { + EXPECT_THAT(result, + IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 1.0}, {y, 0.0}}))); + } else { + EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("solve callback"))); + } +} + +TEST_P(SolveExecutorTest, LogCallback) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + std::vector logs; + EXPECT_THAT( + executor->Solve(model, SolverType::kGlop, + {.message_callback = VectorMessageCallback(&logs)}), + IsOkAndHolds(IsOptimal(1.0))); + EXPECT_THAT(absl::StrJoin(logs, "\n"), HasSubstr("status: OPTIMAL")); +} + +TEST_P(SolveExecutorTest, EnableOutput) { + if (!ScopedStdStreamCapture::kIsSupported) { + GTEST_SKIP() << "Stdout can't be captured."; + } + + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + absl::StatusOr result = executor->Solve( + model, SolverType::kGlop, {.parameters = {.enable_output = true}}); + EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), + HasSubstr("status: OPTIMAL")); + EXPECT_THAT(result, IsOkAndHolds(IsOptimal(1.0))); +} + +TEST_P(SolveExecutorTest, EnableOutputFalseIsSilent) { + if (!ScopedStdStreamCapture::kIsSupported) { + GTEST_SKIP() << "Stdout can't be captured."; + } + + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + absl::StatusOr result = + executor->Solve(model, SolverType::kGlop, {}); + EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), + IsEmpty()); + EXPECT_THAT(result, IsOkAndHolds(IsOptimal(1.0))); +} + +TEST_P(SolveExecutorTest, ExplicitDeadlineAlreadyPassed) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + EXPECT_THAT( + executor->Solve(model, SolverType::kGlop, {}, + {.explicit_deadline = absl::ZeroDuration()}), + AnyOf(CanonicalStatusIs(absl::StatusCode::kDeadlineExceeded), + IsOkAndHolds(TerminatesWith(TerminationReason::kNoSolutionFound)))); +} + +TEST_P(SolveExecutorTest, SolverType) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + executor->New(&model, SolverType::kGlop)); + EXPECT_EQ(solver->solver_type(), SolverType::kGlop); +} + +TEST_P(SolveExecutorTest, IncrementalSolverIsActuallyIncremental) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + SolveParameters params = {.presolve = Emphasis::kOff}; + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + executor->New(&model, SolverType::kGlop)); + EXPECT_THAT(solver->Solve({.parameters = params}), + IsOkAndHolds(IsOptimal(1.0))); + + std::vector logs; + EXPECT_THAT(solver->Solve({.parameters = params, + .message_callback = VectorMessageCallback(&logs)}), + IsOkAndHolds(IsOptimal(1.0))); + const std::string was_incremental = "Starting basis: incremental solve"; + if (features().actual_incrementalism) { + EXPECT_THAT(absl::StrJoin(logs, "\n"), HasSubstr(was_incremental)); + } else { + EXPECT_THAT(absl::StrJoin(logs, "\n"), Not(HasSubstr(was_incremental))); + } +} + +TEST_P(SolveExecutorTest, + IncrementalSolveRespectsAlreadyPassedExplicitDeadline) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + executor->New(&model, SolverType::kGlop, + {.explicit_deadline = absl::ZeroDuration()})); + EXPECT_THAT( + solver->Solve({}), + AnyOf(CanonicalStatusIs(absl::StatusCode::kDeadlineExceeded), + IsOkAndHolds(TerminatesWith(TerminationReason::kNoSolutionFound)))); +} + +TEST_P(SolveExecutorTest, IncrementalSolveRespectsAlreadyCancelled) { + if (GetParam().name == "subprocess_solve") { + GTEST_SKIP() << "Subprocess solve does not guarantee interruptions before " + "the start of a solve are respected, see b/346799566"; + } + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + SolveInterrupter canceller; + canceller.Interrupt(); + ASSERT_OK_AND_ASSIGN( + std::unique_ptr solver, + executor->New(&model, SolverType::kGlop, {.canceller = &canceller})); + EXPECT_THAT(solver->Solve({}), + AnyOf(CanonicalStatusIs(absl::StatusCode::kCancelled), + IsOkAndHolds(TerminatesWithLimit(Limit::kInterrupted)))); +} + +TEST_P(SolveExecutorTest, IncrementalSolverLogCallback) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + std::vector logs; + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + executor->New(&model, SolverType::kGlop)); + EXPECT_THAT(solver->Solve({.message_callback = VectorMessageCallback(&logs)}), + IsOkAndHolds(IsOptimal(1.0))); + EXPECT_THAT(absl::StrJoin(logs, "\n"), HasSubstr("status: OPTIMAL")); +} + +TEST_P(SolveExecutorTest, IncrementalSolverEnableOutput) { + if (!ScopedStdStreamCapture::kIsSupported) { + GTEST_SKIP() << "Stdout can't be captured."; + } + + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + executor->New(&model, SolverType::kGlop)); + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + absl::StatusOr result = + solver->Solve({.parameters = {.enable_output = true}}); + EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), + HasSubstr("status: OPTIMAL")); + EXPECT_THAT(result, IsOkAndHolds(IsOptimal(1.0))); +} + +TEST_P(SolveExecutorTest, IncrementalSolverEnableOutputFalseIsSilent) { + if (!ScopedStdStreamCapture::kIsSupported) { + GTEST_SKIP() << "Stdout can't be captured."; + } + + ASSERT_OK_AND_ASSIGN(std::unique_ptr executor, MakeExecutor()); + Model model; + const Variable x = model.AddContinuousVariable(0, 1, "x"); + model.Maximize(x); + ASSERT_OK_AND_ASSIGN(std::unique_ptr solver, + executor->New(&model, SolverType::kGlop)); + ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); + absl::StatusOr result = solver->Solve(); + EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), + IsEmpty()); + EXPECT_THAT(result, IsOkAndHolds(IsOptimal(1.0))); +} + +} // namespace + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/solve_executor_tests.h b/ortools/math_opt/cpp/executor/solve_executor_tests.h new file mode 100644 index 00000000000..8ccd9088f9e --- /dev/null +++ b/ortools/math_opt/cpp/executor/solve_executor_tests.h @@ -0,0 +1,71 @@ +// 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_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_TESTS_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_TESTS_H_ + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/math_opt/cpp/executor/solve_executor.h" + +namespace operations_research::math_opt { + +// What features are supported/unsupported for a SolveExecutor. +struct SupportedFeatures { + // When interruption is not supported, we just ignore interruptions and + // Solve() calls run to their conclusion. + bool interruption = false; + // When a solve callback is provided but not supported the solve returns an + // error immediately. + bool solve_callback = false; + // Indicates that incremental solves are reusing state from previous solve, + // rather than just solving from scratch. + bool actual_incrementalism = false; +}; + +// Configures tests for an implementation of SolveExecutor. +// +// Implementation note: use `features` to control behavior that is conditional +// in tests. Use GTEST_SKIP only when there is a deficiency in an implementation +// that is worth warning the test runner about (it is noisy in logs). Use the +// name to skip tests only for bugs we can fix, otherwise just add a new +// feature. +struct SolveExecutorTestParameters { + // Display information in test logs. + std::string name; + std::function>()> + executor_provider; + // The functionality supported by SolveExecutor returned from + // executor_provider. + SupportedFeatures features; +}; + +std::ostream& operator<<(std::ostream& ostr, + const SolveExecutorTestParameters& params); + +class SolveExecutorTest + : public testing::TestWithParam { + public: + absl::StatusOr> MakeExecutor() { + return GetParam().executor_provider(); + } + const SupportedFeatures& features() const { return GetParam().features; } +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_SOLVE_EXECUTOR_TESTS_H_ diff --git a/ortools/math_opt/cpp/executor/time_limit_util.cc b/ortools/math_opt/cpp/executor/time_limit_util.cc new file mode 100644 index 00000000000..976e93ec205 --- /dev/null +++ b/ortools/math_opt/cpp/executor/time_limit_util.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/math_opt/cpp/executor/time_limit_util.h" + +#include + +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +void UpdateTimeLimit(absl::Time absolute_deadline, + math_opt::SolveParameters& params) { + UpdateTimeLimit(absolute_deadline - absl::Now(), params); +} + +void UpdateTimeLimit(absl::Duration relative_deadline, + math_opt::SolveParameters& params) { + params.time_limit = std::min( + params.time_limit, std::max(absl::ZeroDuration(), relative_deadline)); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/executor/time_limit_util.h b/ortools/math_opt/cpp/executor/time_limit_util.h new file mode 100644 index 00000000000..a49dc7c5828 --- /dev/null +++ b/ortools/math_opt/cpp/executor/time_limit_util.h @@ -0,0 +1,32 @@ +// 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_MATH_OPT_CPP_EXECUTOR_TIME_LIMIT_UTIL_H_ +#define ORTOOLS_MATH_OPT_CPP_EXECUTOR_TIME_LIMIT_UTIL_H_ + +#include + +#include "absl/time/time.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +void UpdateTimeLimit(absl::Time absolute_deadline, + math_opt::SolveParameters& params); + +void UpdateTimeLimit(absl::Duration relative_deadline, + math_opt::SolveParameters& params); + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_CPP_EXECUTOR_TIME_LIMIT_UTIL_H_ diff --git a/ortools/math_opt/cpp/executor/time_limit_util_test.cc b/ortools/math_opt/cpp/executor/time_limit_util_test.cc new file mode 100644 index 00000000000..2e073d943a0 --- /dev/null +++ b/ortools/math_opt/cpp/executor/time_limit_util_test.cc @@ -0,0 +1,51 @@ +// 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/math_opt/cpp/executor/time_limit_util.h" + +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { +namespace { + +TEST(UpdateTimeLimit, ShrinkWithRelativeDeadline) { + math_opt::SolveParameters params; + UpdateTimeLimit(absl::Seconds(2), params); + EXPECT_EQ(params.time_limit, absl::Seconds(2)); +} + +TEST(UpdateTimeLimit, NoShrinkWithRelativeDeadline) { + math_opt::SolveParameters params; + params.time_limit = absl::Seconds(4); + UpdateTimeLimit(absl::Seconds(8), params); + EXPECT_EQ(params.time_limit, absl::Seconds(4)); +} + +TEST(UpdateTimeLimit, NegativeLimitIsZeroRelativeDeadline) { + math_opt::SolveParameters params; + UpdateTimeLimit(absl::Seconds(-2), params); + EXPECT_EQ(params.time_limit, absl::ZeroDuration()); +} + +TEST(UpdateTimeLimit, AbsoluteDeadline) { + math_opt::SolveParameters params; + UpdateTimeLimit(absl::Now() + absl::Seconds(5), params); + EXPECT_LE(params.time_limit, absl::Seconds(5)); + EXPECT_GE(params.time_limit, absl::Seconds(4)); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc index 9a5e422f5f1..6c73fdef5ca 100644 --- a/ortools/math_opt/cpp/parameters.cc +++ b/ortools/math_opt/cpp/parameters.cc @@ -88,6 +88,8 @@ std::optional Enum::ToOptString( return "santorini"; case SolverType::kXpress: return "xpress"; + case SolverType::kMinCostFlow: + return "min_cost_flow"; } return std::nullopt; } @@ -97,7 +99,7 @@ absl::Span Enum::AllValues() { SolverType::kGscip, SolverType::kGurobi, SolverType::kGlop, SolverType::kCpSat, SolverType::kPdlp, SolverType::kGlpk, SolverType::kEcos, SolverType::kScs, SolverType::kHighs, - SolverType::kSantorini, SolverType::kXpress, + SolverType::kSantorini, SolverType::kXpress, SolverType::kMinCostFlow, }; return absl::MakeConstSpan(kSolverTypeValues); } diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 1f600d09580..42bf3731714 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -115,6 +115,24 @@ enum class SolverType { // Supports LP, MIP, and nonconvex integer quadratic problems. // A fast option, but has special licensing. kXpress = SOLVER_TYPE_XPRESS, + + // Google's Min-Cost Flow solver. + // + // Uses a specialized solver for Min-Cost Flow problems (see + // https://developers.google.com/optimization/flow/mincostflow). Supports LP + // problems that match the structure of a Min-Cost Flow problem (see + // go/mathopt-min-cost-flow). + // + // Requirements: + // * The constraint matrix must be the node-arc incidence matrix of a + // digraph, that is, each variable appears in exactly two constraints, with + // coefficients +1 and -1. + // * Only linear constraints are allowed. + // * All linear constraints must be equality constraints. + // * All variable lower bounds must be 0. + // * All variables and constraints must have integer bounds and costs. + // * The objective must be linear. + kMinCostFlow = SOLVER_TYPE_MIN_COST_FLOW, }; MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED); diff --git a/ortools/math_opt/cpp/streamable_solver_init_arguments.cc b/ortools/math_opt/cpp/streamable_solver_init_arguments.cc index 8c5d1e00082..38c6a9b06f2 100644 --- a/ortools/math_opt/cpp/streamable_solver_init_arguments.cc +++ b/ortools/math_opt/cpp/streamable_solver_init_arguments.cc @@ -18,7 +18,6 @@ #include "absl/status/statusor.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/solvers/gurobi.pb.h" -#include "ortools/math_opt/solvers/xpress.pb.h" namespace operations_research { namespace math_opt { @@ -61,11 +60,22 @@ StreamableGurobiInitArguments StreamableGurobiInitArguments::FromProto( return args; } +XpressInitializerProto::License XpressLicenseKey::Proto() const { + XpressInitializerProto::License license_proto; + license_proto.set_path(path); + return license_proto; +} + +XpressLicenseKey XpressLicenseKey::FromProto( + const XpressInitializerProto::License& license_proto) { + return XpressLicenseKey{.path = license_proto.path()}; +} + XpressInitializerProto StreamableXpressInitArguments::Proto() const { XpressInitializerProto params_proto; - if (extract_names.has_value()) { - params_proto.set_extract_names(extract_names.value()); + if (license.has_value()) { + *params_proto.mutable_license() = license->Proto(); } return params_proto; @@ -74,8 +84,8 @@ XpressInitializerProto StreamableXpressInitArguments::Proto() const { StreamableXpressInitArguments StreamableXpressInitArguments::FromProto( const XpressInitializerProto& args_proto) { StreamableXpressInitArguments args; - if (args_proto.has_extract_names()) { - args.extract_names = args_proto.extract_names(); + if (args_proto.has_license()) { + args.license = XpressLicenseKey::FromProto(args_proto.license()); } return args; } diff --git a/ortools/math_opt/cpp/streamable_solver_init_arguments.h b/ortools/math_opt/cpp/streamable_solver_init_arguments.h index 1552faec8d6..e3b79742c55 100644 --- a/ortools/math_opt/cpp/streamable_solver_init_arguments.h +++ b/ortools/math_opt/cpp/streamable_solver_init_arguments.h @@ -31,6 +31,7 @@ #include "absl/status/statusor.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/solvers/gurobi.pb.h" +#include "ortools/math_opt/solvers/xpress.pb.h" namespace operations_research { namespace math_opt { @@ -78,13 +79,19 @@ struct StreamableGurobiInitArguments { const GurobiInitializerProto& args_proto); }; +// A license key for the Xpress solver. +struct XpressLicenseKey { + std::string path ABSL_REQUIRE_EXPLICIT_INIT; + + XpressInitializerProto::License Proto() const; + static XpressLicenseKey FromProto( + const XpressInitializerProto::License& license_proto); +}; + // Streamable Xpress specific parameters for solver instantiation. struct StreamableXpressInitArguments { - // If present and set to true then variable and constraint names are - // extracted into the underlying Xpress instance. This can help debugging - // (especially if models are exported to disk) but also incurs runtime and - // memory overhead. - std::optional extract_names; + // An optional license key to use to instantiate the solver. + std::optional license; // Returns the proto corresponding to these parameters. XpressInitializerProto Proto() const; diff --git a/ortools/math_opt/docs/snippets/basic_ip.cc b/ortools/math_opt/docs/snippets/basic_ip.cc new file mode 100644 index 00000000000..840833da164 --- /dev/null +++ b/ortools/math_opt/docs/snippets/basic_ip.cc @@ -0,0 +1,95 @@ +// 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. + +// Simple integer programming example for users manual + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { +using ::operations_research::math_opt::LinearConstraint; +using ::operations_research::math_opt::Model; +using ::operations_research::math_opt::SolveResult; +using ::operations_research::math_opt::SolverType; +using ::operations_research::math_opt::Variable; +using ::operations_research::math_opt::VariableMap; + +constexpr double kInf = std::numeric_limits::infinity(); + +void SolveSimpleServerAllocationProblem() { + // [START_build_model] + Model server_model("server allocation"); + + // Variables + const Variable west = server_model.AddIntegerVariable(0.0, kInf, "west"); + const Variable central = + server_model.AddIntegerVariable(0.0, kInf, "central"); + const Variable east = server_model.AddIntegerVariable(0.0, kInf, "east"); + + // Constraints + const LinearConstraint pacific = + server_model.AddLinearConstraint(west + central >= 2, "pacific"); + const LinearConstraint atlantic = + server_model.AddLinearConstraint(central + east >= 1, "atlantic"); + + // Objective + server_model.Minimize(west + 2 * central + 3 * east); + // [END_build_model] + + // [START_solve] + const absl::StatusOr status_or_result = + Solve(server_model, SolverType::kGscip); + CHECK_OK(status_or_result.status()); + const SolveResult& result = status_or_result.value(); + // [END_solve] + + // [START_output_results] + // Check that the problem has an optimal solution. + QCHECK_OK(result.termination.EnsureIsOptimal()); + + // Get some result information + std::cout << "Problem solved in " << result.solve_time() << std::endl; + std::cout << "Objective value: " << result.objective_value() << std::endl; + + // Get solution values + const double west_value = result.variable_values().at(west); + const double east_value = result.variable_values().at(east); + const double central_value = result.variable_values().at(central); + std::cout << "Variable values: [west=" << west_value + << ", central=" << central_value << ", east=" << east_value << "]" + << std::endl; + // [END_output_results] + + // [START_output_ids] + // Print variable and constraint ids. + std::cout << "Variable ids: [west=" << west.id() + << ", central=" << central.id() << ", east=" << east.id() << "]" + << std::endl; + std::cout << "Constraint ids: [pacific=" << pacific.id() + << ", atlantic=" << atlantic.id() << "]" << std::endl; + // [END_output_ids] +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + SolveSimpleServerAllocationProblem(); + return 0; +} diff --git a/ortools/math_opt/docs/snippets/full_mip.cc b/ortools/math_opt/docs/snippets/full_mip.cc new file mode 100644 index 00000000000..9ad4886b1f1 --- /dev/null +++ b/ortools/math_opt/docs/snippets/full_mip.cc @@ -0,0 +1,78 @@ +// 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. + +// Simple integer programming example for users manual + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { +using ::operations_research::math_opt::Model; +using ::operations_research::math_opt::SolveResult; +using ::operations_research::math_opt::SolverType; +using ::operations_research::math_opt::Variable; +using ::operations_research::math_opt::VariableMap; + +constexpr double kInf = std::numeric_limits::infinity(); + +void SolveMIP() { + // [START_full_mip_example] + Model mip_model("server allocation"); + + // Variables + const Variable x_1 = mip_model.AddContinuousVariable(-1.0, 1.5, "x_1"); + const Variable x_2 = mip_model.AddIntegerVariable(0.0, kInf, "x_2"); + const Variable x_3 = mip_model.AddIntegerVariable(-kInf, 10, "x_3"); + + // Constraints, no need to save LinearConstraint objects if we don't use them. + mip_model.AddLinearConstraint(x_1 + x_2 + x_3 <= 12.5, "c_1"); + mip_model.AddLinearConstraint(10 <= x_2 + x_3 <= 11, "c_2"); + mip_model.AddLinearConstraint(x_1 + x_2 >= 2.5, "c_3"); + + // Objective + mip_model.Maximize(3 * x_1 + x_2 + 2 * x_3); + + const absl::StatusOr status_or_result = + Solve(mip_model, SolverType::kGscip); + CHECK_OK(status_or_result.status()); + const SolveResult& result = status_or_result.value(); + + // Check that the problem has an optimal solution. + QCHECK_OK(result.termination.EnsureIsOptimal()); + + // Get some result information + std::cout << "Problem solved in " << result.solve_time() << std::endl; + std::cout << "Objective value: " << result.objective_value() << std::endl; + + // Get solution values + const double x_1_value = result.variable_values().at(x_1); + const double x_2_value = result.variable_values().at(x_2); + const double x_3_value = result.variable_values().at(x_3); + std::cout << "Variable values: [x_1=" << x_1_value << ", x_2=" << x_2_value + << ", x_3=" << x_3_value << "]" << std::endl; + // [END_full_mip_example] +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + SolveMIP(); + return 0; +} diff --git a/ortools/math_opt/docs/snippets/getting_started.cc b/ortools/math_opt/docs/snippets/getting_started.cc new file mode 100644 index 00000000000..9e1aa951bd8 --- /dev/null +++ b/ortools/math_opt/docs/snippets/getting_started.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. + +// Minimal example for Getting Started. +// [START_imports] +#include +#include + +#include "absl/log/check.h" +#include "absl/status/statusor.h" +#include "ortools/base/init_google.h" +#include "ortools/math_opt/cpp/math_opt.h" +// [END_imports] + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + + // [START_build_model] + // Build the model. + namespace math_opt = ::operations_research::math_opt; + math_opt::Model lp_model("getting_started_lp"); + const math_opt::Variable x = lp_model.AddContinuousVariable(-1.0, 1.5, "x"); + const math_opt::Variable y = lp_model.AddContinuousVariable(0.0, 1.0, "y"); + lp_model.AddLinearConstraint(x + y <= 1.5, "c"); + lp_model.Maximize(x + 2 * y); + // [END_build_model] + + // [START_solve_args] + // Set parameters, e.g. turn on logging. + math_opt::SolveArguments args; + args.parameters.enable_output = true; + // [END_solve_args] + + // [START_solve] + // Solve and ensure an optimal solution was found with no errors. + const absl::StatusOr result = + math_opt::Solve(lp_model, math_opt::SolverType::kGlop, args); + CHECK_OK(result.status()); + CHECK_OK(result->termination.EnsureIsOptimal()); + // [END_solve] + + // [START_print_result] + // Print some information from the result. + std::cout << "MathOpt solve succeeded" << std::endl; + std::cout << "Objective value: " << result->objective_value() << std::endl; + std::cout << "x: " << result->variable_values().at(x) << std::endl; + std::cout << "y: " << result->variable_values().at(y) << std::endl; + // [END_print_result] + + return 0; +} diff --git a/ortools/math_opt/docs/snippets/getting_started_py.py b/ortools/math_opt/docs/snippets/getting_started_py.py new file mode 100644 index 00000000000..389ae220476 --- /dev/null +++ b/ortools/math_opt/docs/snippets/getting_started_py.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# 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. + +"""Minimal example for Getting Started page.""" + +from collections.abc import Sequence + +from absl import app + +# [START_imports] +from ortools.math_opt.python import mathopt + +# [END_imports] + + +def main(argv: Sequence[str]) -> None: + del argv # Unused. + + # [START_build_model] + # Build the model. + model = mathopt.Model(name="getting_started_lp") + x = model.add_variable(lb=-1.0, ub=1.5, name="x") + y = model.add_variable(lb=0.0, ub=1.0, name="y") + model.add_linear_constraint(x + y <= 1.5) + model.maximize(x + 2 * y) + # [END_build_model] + + # [START_solve_args] + # Set parameters, e.g. turn on logging. + params = mathopt.SolveParameters(enable_output=True) + # [END_solve_args] + + # [START_solve] + # Solve and ensure an optimal solution was found with no errors. + # (mathopt.solve may raise a RuntimeError on invalid input or internal solver + # errors.) + result = mathopt.solve(model, mathopt.SolverType.GLOP, params=params) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError(f"model failed to solve: {result.termination}") + # [END_solve] + + # [START_print_results] + # Print some information from the result. + print("MathOpt solve succeeded") + print("Objective value:", result.objective_value()) + print("x:", result.variable_values()[x]) + print("y:", result.variable_values()[y]) + # [END_print_results] + + +if __name__ == "__main__": + app.run(main) diff --git a/ortools/math_opt/docs/snippets/mp_solver_migration_test.cc b/ortools/math_opt/docs/snippets/mp_solver_migration_test.cc new file mode 100644 index 00000000000..742445e4d51 --- /dev/null +++ b/ortools/math_opt/docs/snippets/mp_solver_migration_test.cc @@ -0,0 +1,263 @@ +// 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/status/statusor.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/status_macros.h" +#include "ortools/linear_solver/linear_expr.h" +#include "ortools/linear_solver/linear_solver.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/solve_mp_model.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace { + +constexpr double kTolerance = 1e-5; +using ::testing::DoubleNear; +using ::testing::ElementsAre; +using ::testing::status::IsOkAndHolds; + +// [START_solve_with_mpsolver] +absl::StatusOr SolveWithMPSolver() { + using namespace operations_research; // NOLINT + // Build the model. + MPSolver solver("mip", MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING); + const MPVariable* const x_var = solver.MakeBoolVar("x"); + const MPVariable* const y_var = solver.MakeNumVar(0.0, 3.0, "y"); + const LinearExpr x(x_var); + const LinearExpr y(y_var); + solver.MutableObjective()->MaximizeLinearExpr(2 * x + y); + solver.MakeRowConstraint(x + y <= 1.3); + + // Solve the model. + solver.SetTimeLimit(absl::Seconds(10)); + solver.EnableOutput(); + MPSolverParameters parameters; + parameters.SetDoubleParam(MPSolverParameters::RELATIVE_MIP_GAP, 0.05); + const MPSolver::ResultStatus result_status = solver.Solve(parameters); + if (result_status != MPSolver::OPTIMAL && + result_status != MPSolver::FEASIBLE) { + return ortools::InvalidArgumentErrorBuilder() + << "Could not find solution, result status: " << result_status; + } + + // Get the solution. + std::cout << "objective: " << solver.Objective().Value() << "\n" + << "x: " << x_var->solution_value() << "\n" + << "y: " << y_var->solution_value() << std::endl; + return solver.Objective().Value(); +} +// [END_solve_with_mpsolver] + +TEST(MPSolverMigrationTest, SolveWithMPSolver) { + ASSERT_OK_AND_ASSIGN(const double obj, SolveWithMPSolver()); + EXPECT_GE(obj, 0.0 - kTolerance); + EXPECT_LE(obj, 2.3 + kTolerance); +} + +// [START_solve_with_mp_model_proto] +absl::StatusOr SolveWithMPModelProto() { + using namespace operations_research; // NOLINT + MPModelRequest request; + + // Build the model. + MPModelProto& model = *request.mutable_model(); + model.set_maximize(true); + const int x_index = model.variable_size(); + MPVariableProto* const x = model.add_variable(); + x->set_lower_bound(0.0); + x->set_upper_bound(1.0); + x->set_is_integer(true); + x->set_name("x"); + x->set_objective_coefficient(2.0); + const int y_index = model.variable_size(); + MPVariableProto* const y = model.add_variable(); + y->set_lower_bound(0.0); + y->set_upper_bound(3.0); + y->set_is_integer(false); + y->set_name("y"); + y->set_objective_coefficient(1.0); + MPConstraintProto* const c = model.add_constraint(); + c->set_upper_bound(1.3); + c->add_var_index(x_index); + c->add_coefficient(1.0); + c->add_var_index(y_index); + c->add_coefficient(1.0); + + // Solve the model. + request.set_solver_time_limit_seconds(10.0); + request.set_enable_internal_solver_output(true); + request.set_solver_type(MPModelRequest::SCIP_MIXED_INTEGER_PROGRAMMING); + request.set_solver_specific_parameters("limits/gap=0.05"); + const MPSolutionResponse result = SolveMPModel(request); + if (result.status() != MPSOLVER_OPTIMAL && + result.status() != MPSOLVER_FEASIBLE) { + return ortools::InvalidArgumentErrorBuilder() + << "Could not find solution, result status: " + << MPSolverResponseStatus_Name(result.status()); + } + + // Get the solution. + std::cout << "objective: " << result.objective_value() << "\n" + << "x: " << result.variable_value(x_index) << "\n" + << "y: " << result.variable_value(y_index) << std::endl; + return result.objective_value(); +} +// [END_solve_with_mp_model_proto] + +TEST(MPSolverMigrationTest, SolveWithMPModelProto) { + ASSERT_OK_AND_ASSIGN(const double obj, SolveWithMPModelProto()); + EXPECT_GE(obj, 0.0 - kTolerance); + EXPECT_LE(obj, 2.3 + kTolerance); +} + +// [START_solve_with_math_opt] +absl::StatusOr SolveWithMathOpt() { + using namespace operations_research::math_opt; // NOLINT + // Build the model. + Model model("mip"); + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddContinuousVariable(0.0, 3.0, "y"); + model.Maximize(2 * x + y); + model.AddLinearConstraint(x + y <= 1.3); + + // Solve the model. + const SolveParameters params = {.enable_output = true, + .time_limit = absl::Seconds(10), + .relative_gap_tolerance = 0.05}; + OR_ASSIGN_OR_RETURN(const SolveResult result, + Solve(model, SolverType::kGscip, {.parameters = params})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimalOrFeasible()); + + // Get the solution. + std::cout << "objective: " << result.objective_value() << "\n" + << "x: " << result.variable_values().at(x) << "\n" + << "y: " << result.variable_values().at(y) << std::endl; + return result.objective_value(); +} +// [END_solve_with_math_opt] + +TEST(MPSolverMigrationTest, SolveWithMathOpt) { + ASSERT_OK_AND_ASSIGN(const double obj, SolveWithMathOpt()); + EXPECT_GE(obj, 0.0 - kTolerance); + EXPECT_LE(obj, 2.3 + kTolerance); +} + +// [START_solve_incrementally_with_mpsolver] +absl::StatusOr> SolveIncrementallyWithMPSolver() { + using namespace operations_research; // NOLINT + // Build the model. + MPSolver solver("incremental_lp", MPSolver::GLOP_LINEAR_PROGRAMMING); + MPVariable* const x_var = solver.MakeNumVar(0.0, 1.0, "x"); + solver.MutableObjective()->MaximizeLinearExpr(LinearExpr(x_var)); + + // Solve the model. + solver.EnableOutput(); + MPSolverParameters parameters; + // GLOP will only hot-start incremental solves when presolve is disabled. + parameters.SetIntegerParam(MPSolverParameters::PRESOLVE, + MPSolverParameters::PRESOLVE_OFF); + + std::vector objective_values; + for (int i = 0; i < 5; ++i) { + x_var->SetUB(2 * (i + 1)); + const MPSolver::ResultStatus result_status = solver.Solve(parameters); + if (result_status != MPSolver::OPTIMAL) { + return ortools::InvalidArgumentErrorBuilder() + << "Expected OPTIMAL, but result status was: " << result_status; + } + objective_values.push_back(solver.Objective().Value()); + } + return objective_values; +} +// [END_solve_incrementally_with_mpsolver] + +TEST(MPSolverMigrationTest, SolveIncrementallyWithMPSolver) { + EXPECT_THAT( + SolveIncrementallyWithMPSolver(), + IsOkAndHolds(ElementsAre(DoubleNear(2.0, 1e-5), DoubleNear(4.0, 1e-5), + DoubleNear(6.0, 1e-5), DoubleNear(8.0, 1e-5), + DoubleNear(10.0, 1e-5)))); +} +// [START_solve_incrementally_with_math_opt] +absl::StatusOr> SolveIncrementallyWithMathOpt() { + using namespace operations_research::math_opt; // NOLINT + // Build the model. + Model model("incremental_lp"); + const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); + model.Maximize(x); + + // Solve the model. + // GLOP will only hot-start incremental solves when presolve is disabled. + const SolveParameters params = {.enable_output = true, + .presolve = Emphasis::kOff}; + OR_ASSIGN_OR_RETURN(const std::unique_ptr solver, + NewIncrementalSolver(&model, SolverType::kGlop)); + std::vector objective_values; + for (int i = 0; i < 5; ++i) { + model.set_upper_bound(x, 2 * (i + 1)); + OR_ASSIGN_OR_RETURN(const SolveResult result, + solver->Solve({.parameters = params})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + objective_values.push_back(result.objective_value()); + } + return objective_values; +} +// [END_solve_incrementally_with_math_opt] + +// [START_solve_incrementally_with_math_opt_test] +TEST(MPSolverMigrationTest, SolveIncrementallyWithMathOpt) { + EXPECT_THAT( + SolveIncrementallyWithMathOpt(), + IsOkAndHolds(ElementsAre(DoubleNear(2.0, 1e-5), DoubleNear(4.0, 1e-5), + DoubleNear(6.0, 1e-5), DoubleNear(8.0, 1e-5), + DoubleNear(10.0, 1e-5)))); +} +// [END_solve_incrementally_with_math_opt_test] + +// [START_mpsolver_integer_solution_value] +TEST(MPSolverMigrationTest, MPSolverIntegerSolutionValue) { + using namespace operations_research; // NOLINT + // Build the model. + MPSolver solver("mip", MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING); + const MPVariable* const x_var = solver.MakeBoolVar("x"); + solver.MutableObjective()->MaximizeLinearExpr(LinearExpr(x_var)); + const MPSolver::ResultStatus result_status = solver.Solve(); + ASSERT_EQ(result_status, MPSolver::OPTIMAL); + EXPECT_EQ(x_var->solution_value(), 1.0); +} +// [END_mpsolver_integer_solution_value] + +// [START_math_opt_integer_solution_value] +TEST(MPSolverMigrationTest, MathOptIntegerSolutionValue) { + using namespace operations_research::math_opt; // NOLINT + // Build the model. + Model model("mip"); + const Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + ASSERT_OK_AND_ASSIGN(const SolveResult result, + Solve(model, SolverType::kGscip)); + ASSERT_EQ(result.termination.reason, TerminationReason::kOptimal); + EXPECT_EQ(std::round(result.variable_values().at(x)), 1.0); +} +// [END_math_opt_integer_solution_value] + +} // namespace diff --git a/ortools/math_opt/docs/snippets/solve_callback_stream_solutions.cc b/ortools/math_opt/docs/snippets/solve_callback_stream_solutions.cc new file mode 100644 index 00000000000..07363a92ec7 --- /dev/null +++ b/ortools/math_opt/docs/snippets/solve_callback_stream_solutions.cc @@ -0,0 +1,69 @@ +// 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. + +// [START solve_callback_stream_solutions_cc_include] +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/synchronization/mutex.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +// [END solve_callback_stream_solutions_cc_include] + +namespace { + +absl::Status Main() { + // [START solve_callback_stream_solutions] + namespace math_opt = operations_research::math_opt; + math_opt::Model model; + const math_opt::Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + absl::Mutex cb_mutex; + auto callback = [x, &cb_mutex](const math_opt::CallbackData& cb_data) { + CHECK_EQ(cb_data.event, math_opt::CallbackEvent::kMipSolution); + // Warning: either the callback should be threadsafe, or you must rely on + // solver specific guarantees that the callback isn't invoked concurrently. + const absl::MutexLock lock(cb_mutex); + std::cout << "Found solution x = " << cb_data.solution->at(x) << std::endl; + return math_opt::CallbackResult(); + }; + const math_opt::CallbackRegistration cb_reg = { + .events = {math_opt::CallbackEvent::kMipSolution}}; + // Gurobi, CpSat, and SCIP all support this callback. + const math_opt::SolverType solver = math_opt::SolverType::kGscip; + OR_ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, solver, + {.callback_registration = cb_reg, + .callback = std::move(callback)})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + std::cout << "Optimal x = " << result.variable_values().at(x) << std::endl; + return absl::OkStatus(); + // [END solve_callback_stream_solutions] +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/docs/snippets/solve_callback_stream_solutions_py.py b/ortools/math_opt/docs/snippets/solve_callback_stream_solutions_py.py new file mode 100644 index 00000000000..0cc4eccfabf --- /dev/null +++ b/ortools/math_opt/docs/snippets/solve_callback_stream_solutions_py.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# 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. + +"""Minimal example of getting a callback on every integer solution.""" + +import threading +# [START solve_callback_stream_solutions_py_imports] +from collections.abc import Sequence + +from absl import app + +from ortools.math_opt.python import mathopt + +# [END solve_callback_stream_solutions_py_imports] + + +def main(argv: Sequence[str]) -> None: + del argv # Unused. + + # [START solve_callback_stream_solutions_py] + model = mathopt.Model() + x = model.add_binary_variable(name="x") + model.maximize(x) + + cb_mutex = threading.Lock() + + def callback(cb_data: mathopt.CallbackData) -> mathopt.CallbackResult: + # Warning: either the callback should be threadsafe, or you must rely on + # solver specific guarantees that the callback isn't invoked concurrently. + with cb_mutex: + if cb_data.event != mathopt.Event.MIP_SOLUTION: + raise RuntimeError(f"callback event not SOLUTION, was: {cb_data.event}") + x_value = cb_data.solution[x] + print(f"Found solution x = {x_value}", flush=True) + return mathopt.CallbackResult() + + cb_reg = mathopt.CallbackRegistration(events={mathopt.Event.MIP_SOLUTION}) + # Gurobi, CpSat, and SCIP all support this callback. + solver = mathopt.SolverType.GSCIP + result = mathopt.solve(model, solver, callback_reg=cb_reg, cb=callback) + + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError(f"model failed to solve: {result.termination}") + print(f"Optimal x = {result.variable_values(x)}", flush=True) + # [END solve_callback_stream_solutions_py] + + +if __name__ == "__main__": + app.run(main) diff --git a/ortools/math_opt/docs/snippets/solve_logs_test.cc b/ortools/math_opt/docs/snippets/solve_logs_test.cc new file mode 100644 index 00000000000..a234a7f2dfb --- /dev/null +++ b/ortools/math_opt/docs/snippets/solve_logs_test.cc @@ -0,0 +1,157 @@ +// 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 "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/status/statusor.h" +#include "absl/synchronization/mutex.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/log_severity.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/port/scoped_std_stream_capture.h" +#include "testing/base/public/mock-log.h" + +namespace { + +using ::testing::_; +using ::testing::Contains; +using ::testing::DoubleNear; +using ::testing::HasSubstr; +using ::testing::kDoNotCaptureLogsYet; +using ::testing::ScopedMockLog; +using ::testing::status::IsOkAndHolds; + +namespace math_opt = ::operations_research::math_opt; + +// [START_print_logs_with_solve_parameters] + +// Solves +// max x +// s.t. x in {0, 1} +// to optimality, returns the objective value, and prints the logs to standard +// output. +absl::StatusOr PrintLogsWithSolveParameters() { + math_opt::Model model; + const math_opt::Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + + const math_opt::SolveParameters params = {.enable_output = true}; + OR_ASSIGN_OR_RETURN(const math_opt::SolveResult result, + math_opt::Solve(model, math_opt::SolverType::kGscip, + {.parameters = params})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + return result.objective_value(); +} +// [END_print_logs_with_solve_parameters] + +TEST(SolveLogsTest, PrintLogsWithSolveParameters) { + if (!operations_research::ScopedStdStreamCapture::kIsSupported) { + GTEST_SKIP() << "Stdout can't be captured."; + } + + absl::StatusOr objective_value = 0.0; + { + operations_research::ScopedStdStreamCapture stdout_capture( + operations_research::CapturedStream::kStdout); + objective_value = PrintLogsWithSolveParameters(); + if (objective_value.ok()) { + EXPECT_THAT( + std::move(stdout_capture).StopCaptureAndReturnContents(), + testing::ContainsRegex("problem is solved.*optimal solution found")); + } + } + ASSERT_THAT(objective_value, IsOkAndHolds(DoubleNear(1.0, 1.0e-5))); +} + +// [START_canned_message_callback] + +// Solves +// max x +// s.t. x in {0, 1} +// to optimality and prints the logs to LOG(INFO) with a canned message +// callback, then returns the optimal objective value. +absl::StatusOr CannedMessageCallback() { + math_opt::Model model; + const math_opt::Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + + // See message_callback.h for other canned callbacks. + OR_ASSIGN_OR_RETURN( + const math_opt::SolveResult result, + math_opt::Solve(model, math_opt::SolverType::kGscip, + {.message_callback = math_opt::InfoLoggerMessageCallback( + "my_application_prefix: ")})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + return result.objective_value(); +} +// [END_canned_message_callback] + +TEST(SolveLogsTest, CannedMessageCallbackLogs) { + ScopedMockLog log(kDoNotCaptureLogsYet); + EXPECT_CALL(log, Log).Times(testing::AnyNumber()); + EXPECT_CALL(log, + Log(base_logging::INFO, _, + testing::MatchesRegex("my_application_prefix.*problem is " + "solved.*optimal solution found.*"))); + log.StartCapturingLogs(); + absl::StatusOr objective_value = CannedMessageCallback(); + log.StopCapturingLogs(); + testing::Mock::VerifyAndClearExpectations(&log); + ASSERT_THAT(objective_value, IsOkAndHolds(DoubleNear(1.0, 1.0e-5))); +} + +// [START_solve_and_return_logs] + +// Solves +// max x +// s.t. x in {0, 1} +// to optimality and returns the solver logs on success. +absl::StatusOr> SolveAndReturnLogs() { + math_opt::Model model; + const math_opt::Variable x = model.AddBinaryVariable("x"); + model.Maximize(x); + + // The logging callback is not threadsafe for every solver. + absl::Mutex mutex; + std::vector logs; + // Note: `VectorMessageCallback()` in message_callback.h does exactly this, we + // write the callback by hand only as a demonstration. + auto message_cb = [&mutex, &logs](const std::vector& msgs) { + absl::MutexLock lock(mutex); + for (const std::string& msg : msgs) { + logs.push_back(msg); + } + }; + OR_ASSIGN_OR_RETURN( + const math_opt::SolveResult result, + math_opt::Solve(model, math_opt::SolverType::kGscip, + {.message_callback = std::move(message_cb)})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + return logs; +} +// [END_solve_and_return_logs] + +TEST(SolveLogsTest, SolveAndReturnLogsHasLogs) { + ASSERT_OK_AND_ASSIGN(const std::vector logs, + SolveAndReturnLogs()); + ASSERT_THAT( + logs, Contains(HasSubstr("problem is solved [optimal solution found]"))); +} + +} // namespace diff --git a/ortools/math_opt/elemental/BUILD.bazel b/ortools/math_opt/elemental/BUILD.bazel index 2fcb2aaeba4..353288b28ce 100644 --- a/ortools/math_opt/elemental/BUILD.bazel +++ b/ortools/math_opt/elemental/BUILD.bazel @@ -11,13 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +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") cc_library( name = "attributes", hdrs = ["attributes.h"], - visibility = ["//ortools/math_opt:__subpackages__"], + visibility = [ + "//ortools/math_opt:__subpackages__", + "//third_party/ortools/ortools:__subpackages__", + ], deps = [ ":arrays", ":elements", @@ -47,7 +51,10 @@ cc_library( "elemental_to_string.cc", ], hdrs = ["elemental.h"], - visibility = ["//ortools/math_opt:__subpackages__"], + visibility = [ + "//ortools/math_opt:__subpackages__", + "//third_party/ortools/ortools:__subpackages__", + ], deps = [ ":arrays", ":attr_key", @@ -94,11 +101,25 @@ cc_test( ":elemental", ":elemental_matcher", ":elements", - ":symmetry", - ":testing", "//ortools/base:gmock_main", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", + ], +) + +cc_binary( + name = "elemental_benchmark", + testonly = 1, + srcs = ["elemental_benchmark.cc"], + deps = [ + ":attr_key", + ":attributes", + ":elemental", + ":elements", + ":symmetry", + ":testing", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/log:check", "@google_benchmark//:benchmark", ], ) @@ -106,7 +127,10 @@ cc_test( cc_library( name = "derived_data", hdrs = ["derived_data.h"], - visibility = ["//ortools/math_opt:__subpackages__"], + visibility = [ + "//ortools/math_opt:__subpackages__", + "//third_party/ortools/ortools:__subpackages__", + ], deps = [ ":arrays", ":attr_key", @@ -153,6 +177,16 @@ cc_test( ":element_storage", "//ortools/base:gmock_main", "@abseil-cpp//absl/status", + ], +) + +cc_binary( + name = "element_storage_benchmark", + testonly = 1, + srcs = ["element_storage_benchmark.cc"], + deps = [ + ":element_storage", + "//ortools/base:benchmark_main", "@google_benchmark//:benchmark", ], ) @@ -221,6 +255,18 @@ cc_test( ":attr_storage", ":symmetry", "//ortools/base:gmock_main", + ], +) + +cc_binary( + name = "attr_storage_benchmark", + testonly = 1, + srcs = ["attr_storage_benchmark.cc"], + deps = [ + ":attr_key", + ":attr_storage", + ":symmetry", + "//ortools/base:benchmark_main", "@google_benchmark//:benchmark", ], ) @@ -248,7 +294,10 @@ cc_test( cc_library( name = "attr_key", hdrs = ["attr_key.h"], - visibility = ["//ortools/math_opt:__subpackages__"], + visibility = [ + "//ortools/math_opt:__subpackages__", + "//third_party/ortools/ortools:__subpackages__", + ], deps = [ ":elements", ":symmetry", @@ -268,16 +317,27 @@ cc_test( ":attr_key", ":elements", ":symmetry", - ":testing", "//ortools/base:gmock_main", "//ortools/math_opt/testing:stream", - "@abseil-cpp//absl/algorithm:container", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/hash:hash_testing", "@abseil-cpp//absl/meta:type_traits", "@abseil-cpp//absl/status", "@abseil-cpp//absl/strings", + ], +) + +cc_binary( + name = "attr_key_benchmark", + testonly = 1, + srcs = ["attr_key_benchmark.cc"], + deps = [ + ":attr_key", + ":symmetry", + ":testing", + "//ortools/base:benchmark_main", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", "@google_benchmark//:benchmark", ], ) @@ -285,7 +345,10 @@ cc_test( cc_library( name = "arrays", hdrs = ["arrays.h"], - visibility = ["//ortools/math_opt/elemental:__subpackages__"], + visibility = [ + "//ortools/math_opt/elemental/codegen:__pkg__", + "//ortools/math_opt/elemental/python:__pkg__", + ], ) cc_library( @@ -377,7 +440,9 @@ cc_test( cc_library( name = "safe_attr_ops", hdrs = ["safe_attr_ops.h"], - visibility = ["//ortools/math_opt/elemental/c_api:__subpackages__"], + visibility = [ + "//ortools/math_opt/elemental/c_api:__subpackages__", + ], deps = [ ":derived_data", ":elemental", diff --git a/ortools/math_opt/elemental/CMakeLists.txt b/ortools/math_opt/elemental/CMakeLists.txt index 8f598fb2306..b8166e36a75 100644 --- a/ortools/math_opt/elemental/CMakeLists.txt +++ b/ortools/math_opt/elemental/CMakeLists.txt @@ -15,6 +15,7 @@ set(NAME ${PROJECT_NAME}_math_opt_elemental) add_library(${NAME} OBJECT) file(GLOB_RECURSE _SRCS "*.h" "*.cc") +list(FILTER _SRCS EXCLUDE REGEX ".*/.*_benchmark.cc") list(FILTER _SRCS EXCLUDE REGEX ".*/.*_test.cc") list(FILTER _SRCS EXCLUDE REGEX "/elemental_matcher.*") list(FILTER _SRCS EXCLUDE REGEX "/python/.*") diff --git a/ortools/math_opt/elemental/attr_key_benchmark.cc b/ortools/math_opt/elemental/attr_key_benchmark.cc new file mode 100644 index 00000000000..6e049bf2704 --- /dev/null +++ b/ortools/math_opt/elemental/attr_key_benchmark.cc @@ -0,0 +1,97 @@ +// 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 "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "benchmark/benchmark.h" +#include "ortools/math_opt/elemental/attr_key.h" +#include "ortools/math_opt/elemental/symmetry.h" +#include "ortools/math_opt/elemental/testing.h" + +namespace operations_research::math_opt { +namespace { + +constexpr int kBenchmarkSize = 30; + +template +void BM_HashSet0(benchmark::State& state) { + SetT set; + for (const auto s : state) { + auto it = set.find(AttrKey()); + benchmark::DoNotOptimize(it); + } +} +BENCHMARK(BM_HashSet0>>); +BENCHMARK(BM_HashSet0>>); + +template +void BM_HashMap1(benchmark::State& state) { + absl::flat_hash_map map; + for (int i = 0; i < kBenchmarkSize * kBenchmarkSize; ++i) { + if (i % 2 > 0) { // Half of the lookups are hits. + map[T(i)] = i; + } + } + for (const auto s : state) { + for (int i = 0; i < kBenchmarkSize * kBenchmarkSize; ++i) { + auto it = map.find(T(i)); + benchmark::DoNotOptimize(it); + } + } +} +BENCHMARK(BM_HashMap1>); +BENCHMARK(BM_HashMap1); + +template +void BM_HashMap2(benchmark::State& state) { + absl::flat_hash_map map; + for (int i = 0; i < kBenchmarkSize; ++i) { + for (int j = 0; j < kBenchmarkSize; ++j) { + if ((i * kBenchmarkSize + j) % 2 > 0) { // Half of the lookups are hits. + map[T(i, j)] = i; + } + } + } + for (const auto s : state) { + for (int i = 0; i < kBenchmarkSize; ++i) { + for (int j = 0; j < kBenchmarkSize; ++j) { + auto it = map.find(T(i, j)); + benchmark::DoNotOptimize(it); + } + } + } +} +BENCHMARK(BM_HashMap2>); +BENCHMARK(BM_HashMap2>); + +template +void BM_SortAttrKeys(benchmark::State& state) { + const std::vector> keys = + MakeRandomAttrKeys(state.range(0), state.range(0)); + + for (const auto s : state) { + auto copy = keys; + absl::c_sort(copy); + benchmark::DoNotOptimize(copy); + } +} +BENCHMARK(BM_SortAttrKeys<1>)->Arg(100)->Arg(10000); +BENCHMARK(BM_SortAttrKeys<2>)->Arg(100)->Arg(10000); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/elemental/attr_storage_benchmark.cc b/ortools/math_opt/elemental/attr_storage_benchmark.cc new file mode 100644 index 00000000000..3d11e896b2f --- /dev/null +++ b/ortools/math_opt/elemental/attr_storage_benchmark.cc @@ -0,0 +1,190 @@ +// 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 "benchmark/benchmark.h" +#include "ortools/math_opt/elemental/attr_key.h" +#include "ortools/math_opt/elemental/attr_storage.h" +#include "ortools/math_opt/elemental/symmetry.h" + +namespace operations_research::math_opt { +namespace { + +// Makes a set of `n` 1-dimensional keys. +std::vector> Make1DKeys(int n) { + std::vector> keys; + for (int64_t i = 0; i < n; ++i) { + keys.emplace_back(i); + } + return keys; +} + +// Makes a set of `n^2` 2-dimensional keys. +// NOTE: depending in `Symmetry` this might create duplicate keys. This is +// intentional, as we want to have the same number of keys to be able to compare +// the performance of different symmetries. +template +std::vector> Make2DKeys(int n) { + std::vector> keys; + for (int64_t i = 0; i < n; ++i) { + for (int64_t j = 0; j < n; ++j) { + keys.emplace_back(i, j); + } + } + return keys; +} + +// A functor that returns true every N calls, false otherwise. +template +struct TrueEvery { + int n = 0; + bool operator()() { + if (n == N) { + n = 0; + return true; + } + ++n; + return false; + } +}; + +void BM_Attr0StorageSet(benchmark::State& state) { + AttrStorage attr_storage(1.0); + + for (const auto s : state) { + attr_storage.Set(AttrKey(), 10.0); + benchmark::DoNotOptimize(attr_storage); + } +} +BENCHMARK(BM_Attr0StorageSet); + +void BM_Attr1StorageSet(benchmark::State& state) { + const int n = state.range(0); + + AttrStorage attr_storage(1.0); + const auto keys = Make1DKeys(n); + + for (const auto s : state) { + for (const auto& key : keys) { + attr_storage.Set(key, 10.0); + } + } +} +BENCHMARK(BM_Attr1StorageSet)->Arg(900); + +template +void BM_Attr2StorageSet(benchmark::State& state) { + const int n = state.range(0); + + const auto keys = Make2DKeys(n); + + std::optional> attr_storage(1.0); + for (const auto s : state) { + for (const auto& key : keys) { + attr_storage->Set(key, 10.0); + } + state.PauseTiming(); + attr_storage.emplace(1.0); + state.ResumeTiming(); + } +} +BENCHMARK(BM_Attr2StorageSet)->Arg(30); +BENCHMARK(BM_Attr2StorageSet>)->Arg(30); + +void BM_Attr0StorageGet(benchmark::State& state) { + AttrStorage attr_storage(1.0); + + for (const auto s : state) { + double v = attr_storage.Get(AttrKey()); + benchmark::DoNotOptimize(v); + } +} +BENCHMARK(BM_Attr0StorageGet); + +void BM_Attr1StorageGet(benchmark::State& state) { + const int n = state.range(0); + + AttrStorage attr_storage(1.0); + const auto keys = Make1DKeys(n); + // Insert half the keys. + TrueEvery<2> sample; + for (const auto& key : keys) { + if (sample()) { + attr_storage.Set(key, 10.0); + } + } + + for (const auto s : state) { + for (const auto& key : keys) { + double v = attr_storage.Get(key); + benchmark::DoNotOptimize(v); + } + } +} +BENCHMARK(BM_Attr1StorageGet)->Arg(900); + +template +void BM_Attr2StorageGet(benchmark::State& state) { + const int n = state.range(0); + + AttrStorage attr_storage(1.0); + const auto keys = Make2DKeys(n); + // Insert half the keys. + TrueEvery<2> sample; + for (const auto& key : keys) { + if (sample()) { + attr_storage.Set(key, 10.0); + } + } + + for (const auto s : state) { + for (const auto& key : keys) { + double v = attr_storage.Get(key); + benchmark::DoNotOptimize(v); + } + } +} +BENCHMARK(BM_Attr2StorageGet)->Arg(30); +BENCHMARK(BM_Attr2StorageGet>)->Arg(30); + +template +void BM_Attr2StorageSlice(benchmark::State& state) { + const int n = state.range(0); + + AttrStorage attr_storage(1.0); + const auto keys = Make2DKeys(n); + // Insert 5% of the keys. + TrueEvery<20> sample; + for (const auto& key : keys) { + if (sample()) { + attr_storage.Set(key, 10.0); + } + } + + for (const auto s : state) { + for (int key_id = 0; key_id < n; ++key_id) { + auto slice0 = attr_storage.template Slice<0>(key_id); + auto slice1 = attr_storage.template Slice<1>(key_id); + benchmark::DoNotOptimize(slice0); + benchmark::DoNotOptimize(slice1); + } + } +} +BENCHMARK(BM_Attr2StorageSlice)->Arg(30); +BENCHMARK(BM_Attr2StorageSlice>)->Arg(30); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/elemental/codegen/BUILD.bazel b/ortools/math_opt/elemental/codegen/BUILD.bazel index 8977470617c..45d23614519 100644 --- a/ortools/math_opt/elemental/codegen/BUILD.bazel +++ b/ortools/math_opt/elemental/codegen/BUILD.bazel @@ -68,7 +68,7 @@ cc_binary( srcs = ["codegen.cc"], visibility = [ "//ortools/math_opt/elemental/c_api:__subpackages__", - "//ortools/math_opt/elemental/python:__subpackages__", + "//ortools/math_opt/elemental/python:__pkg__", ], deps = [ ":gen", diff --git a/ortools/math_opt/elemental/element_storage_benchmark.cc b/ortools/math_opt/elemental/element_storage_benchmark.cc new file mode 100644 index 00000000000..7731a6ad623 --- /dev/null +++ b/ortools/math_opt/elemental/element_storage_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 "benchmark/benchmark.h" +#include "ortools/math_opt/elemental/element_storage.h" + +namespace operations_research::math_opt { +namespace { + +void BM_AddElements(benchmark::State& state) { + const int n = state.range(0); + for (auto s : state) { + ElementStorage storage; + for (int i = 0; i < n; ++i) { + storage.Add(""); + } + benchmark::DoNotOptimize(storage); + } +} + +BENCHMARK(BM_AddElements)->Arg(100)->Arg(10000); + +void BM_Exists(benchmark::State& state) { + const int n = state.range(0); + ElementStorage storage; + for (int i = 0; i < n; ++i) { + storage.Add(""); + } + for (auto s : state) { + for (int i = 0; i < 2 * n; ++i) { + bool e = storage.Exists(i); + benchmark::DoNotOptimize(e); + } + } +} + +BENCHMARK(BM_Exists)->Arg(100)->Arg(10000); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/elemental/elemental_benchmark.cc b/ortools/math_opt/elemental/elemental_benchmark.cc new file mode 100644 index 00000000000..572be218fb6 --- /dev/null +++ b/ortools/math_opt/elemental/elemental_benchmark.cc @@ -0,0 +1,108 @@ +// 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/log/check.h" +#include "benchmark/benchmark.h" +#include "ortools/math_opt/elemental/attr_key.h" +#include "ortools/math_opt/elemental/attributes.h" +#include "ortools/math_opt/elemental/elemental.h" +#include "ortools/math_opt/elemental/elements.h" +#include "ortools/math_opt/elemental/symmetry.h" +#include "ortools/math_opt/elemental/testing.h" + +namespace operations_research::math_opt { +namespace { + +template +void BM_RandomGet(benchmark::State& state) { + const int n = state.range(0); + Elemental elemental; + // Create a model with n variables and n constraints, attributes on all + // variables and all (variable x constraint). + std::vector vars; + std::vector constraints; + for (int i = 0; i < n; ++i) { + vars.push_back(elemental.AddElement("")); + constraints.push_back( + elemental.AddElement("")); + } + elemental.SetAttr(BoolAttr0::kMaximize, AttrKey(), true); + for (int i = 0; i < n; ++i) { + elemental.SetAttr(DoubleAttr1::kVarLb, AttrKey(vars[i]), 43.0); + for (int j = 0; j < n; ++j) { + elemental.SetAttr(DoubleAttr2::kLinConCoef, + AttrKey(vars[i], constraints[j]), 42.0); + } + } + constexpr int kNumKeys = 1000; + const auto keys = MakeRandomAttrKeys(kNumKeys, n); + for (auto s : state) { + for (const auto& key : keys) { + if constexpr (dimension == 0) { + auto v = elemental.GetAttr(BoolAttr0::kMaximize, key); + benchmark::DoNotOptimize(v); + } else if constexpr (dimension == 1) { + auto v = elemental.GetAttr(DoubleAttr1::kVarLb, key); + benchmark::DoNotOptimize(v); + } else if constexpr (dimension == 2) { + auto v = elemental.GetAttr(DoubleAttr2::kLinConCoef, key); + benchmark::DoNotOptimize(v); + } + } + } +} +BENCHMARK(BM_RandomGet<0>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<1>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<2>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<0, Elemental::StatusPolicy>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<1, Elemental::StatusPolicy>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<2, Elemental::StatusPolicy>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<0, Elemental::UBPolicy>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<1, Elemental::UBPolicy>)->Arg(1)->Arg(10)->Arg(100); +BENCHMARK(BM_RandomGet<2, Elemental::UBPolicy>)->Arg(1)->Arg(10)->Arg(100); + +void BM_DeleteElement(benchmark::State& state) { + const int n = state.range(0); + constexpr int kNumKeys = 100; + constexpr auto kAttr = DoubleAttr2::kLinConCoef; + const auto keys = MakeRandomAttrKeys<2, NoSymmetry>(kNumKeys, n); + for (auto s : state) { + state.PauseTiming(); + auto elemental = std::make_unique(); + for (int i = 0; i < n; ++i) { + elemental->AddElement(""); + elemental->AddElement(""); + } + for (int v = 0; v < n; ++v) { + for (int c = 0; c < n; ++c) { + elemental->SetAttr(kAttr, AttrKey(c, v), 42.0); + } + } + state.ResumeTiming(); + for (int v = 0; v < n; ++v) { + elemental->DeleteElement(VariableId(v)); + } + state.PauseTiming(); + CHECK_EQ(elemental->AttrNonDefaults(kAttr).size(), 0); + elemental.reset(); + state.ResumeTiming(); + } +} +BENCHMARK(BM_DeleteElement)->Arg(10)->Arg(100); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/elemental/python/BUILD.bazel b/ortools/math_opt/elemental/python/BUILD.bazel index dcb5d785ea6..29149b895c9 100644 --- a/ortools/math_opt/elemental/python/BUILD.bazel +++ b/ortools/math_opt/elemental/python/BUILD.bazel @@ -21,14 +21,17 @@ genrule( outs = [ "enums.py", ], - cmd = "$(location //ortools/math_opt/elemental/codegen:codegen) --binding_type=python_enums > $@", + cmd = "$(location //ortools/math_opt/elemental/codegen) --binding_type=python_enums > $@", tools = ["//ortools/math_opt/elemental/codegen"], ) py_library( name = "enums", srcs = [":generated_enums"], - visibility = ["//ortools/math_opt/python:__subpackages__"], + visibility = [ + "//ortools/math_opt:__subpackages__", + "//ortools/math_opt/python:__subpackages__", + ], deps = [ requirement("numpy"), ], diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index aedb4172b67..e4bcc73ddc8 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -18,9 +18,9 @@ package operations_research.math_opt; import "google/protobuf/duration.proto"; import "ortools/glop/parameters.proto"; +import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/pdlp/solvers.proto"; import "ortools/math_opt/solvers/glpk.proto"; -import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/math_opt/solvers/gurobi.proto"; import "ortools/math_opt/solvers/highs.proto"; import "ortools/math_opt/solvers/osqp.proto"; @@ -115,6 +115,24 @@ enum SolverTypeProto { // Supports LP, MIP, and nonconvex integer quadratic problems. // A fast option, but has special licensing. SOLVER_TYPE_XPRESS = 13; + + // Google's Min-Cost Flow solver. + // + // Uses a specialized solver for Min-Cost Flow problems (see + // https://developers.google.com/optimization/flow/mincostflow). Supports LP + // problems that match the structure of a Min-Cost Flow problem (see + // go/mathopt-min-cost-flow). + // + // Requirements: + // * The constraint matrix must be the node-arc incidence matrix of a + // digraph, that is, each variable appears in exactly two constraints, with + // coefficients +1 and -1. + // * Only linear constraints are allowed. + // * All linear constraints must be equality constraints. + // * All variable lower bounds must be 0. + // * All variables and constraints must have integer bounds and costs. + // * The objective must be linear. + SOLVER_TYPE_MIN_COST_FLOW = 14; } // Selects an algorithm for solving linear programs. @@ -166,13 +184,6 @@ enum EmphasisProto { EMPHASIS_VERY_HIGH = 5; } -// Configures if potentially bad solver input is a warning or an error. -// -// TODO(b/196132970): implement this feature. -message StrictnessProto { - bool bad_parameter = 1; -} - // This message contains solver specific data that are used when the solver is // instantiated. message SolverInitializerProto { diff --git a/ortools/math_opt/python/BUILD.bazel b/ortools/math_opt/python/BUILD.bazel index 8ea801335a9..7c72c450fc8 100644 --- a/ortools/math_opt/python/BUILD.bazel +++ b/ortools/math_opt/python/BUILD.bazel @@ -289,6 +289,7 @@ py_library( "//ortools/math_opt/solvers:gurobi_py_pb2", "//ortools/math_opt/solvers:highs_py_pb2", "//ortools/math_opt/solvers:osqp_py_pb2", + "//ortools/math_opt/solvers:xpress_py_pb2", "//ortools/math_opt/solvers/gscip:gscip_py_pb2", "//ortools/pdlp:solvers_py_pb2", "//ortools/sat:sat_parameters_py_pb2", @@ -309,6 +310,7 @@ py_test( "//ortools/math_opt/solvers:gurobi_py_pb2", "//ortools/math_opt/solvers:highs_py_pb2", "//ortools/math_opt/solvers:osqp_py_pb2", + "//ortools/math_opt/solvers:xpress_py_pb2", "//ortools/math_opt/solvers/gscip:gscip_py_pb2", "//ortools/pdlp:solvers_py_pb2", "//ortools/sat:sat_parameters_py_pb2", @@ -414,8 +416,8 @@ py_library( ":parameters", ":result", "//ortools/math_opt:parameters_py_pb2", - "//ortools/math_opt:rpc_py_pb2", "//ortools/math_opt/core/python:solver", + "//ortools/util:status_py_pb2", "//ortools/util/python:solve_interrupter", ], ) @@ -562,7 +564,10 @@ py_test( py_library( name = "errors", srcs = ["errors.py"], - deps = ["//ortools/math_opt:rpc_py_pb2"], + deps = [ + "//ortools/util:status_py_pb2", + "//ortools/util/python:status_streaming", + ], ) py_test( @@ -571,7 +576,8 @@ py_test( deps = [ ":errors", requirement("absl-py"), - "//ortools/math_opt:rpc_py_pb2", + "//ortools/util:status_py_pb2", + "//ortools/util/python:status_streaming", ], ) diff --git a/ortools/math_opt/python/errors.py b/ortools/math_opt/python/errors.py index 6092a23abb1..f6cb0cc6a70 100644 --- a/ortools/math_opt/python/errors.py +++ b/ortools/math_opt/python/errors.py @@ -18,32 +18,10 @@ instead implemented in Python. This will give Python users a more familiar API. """ -import enum -from typing import Optional, Type +from typing import Optional -from ortools.math_opt import rpc_pb2 - - -class _StatusCode(enum.Enum): - """The C++ absl::Status::code() values.""" - - OK = 0 - CANCELLED = 1 - UNKNOWN = 2 - INVALID_ARGUMENT = 3 - DEADLINE_EXCEEDED = 4 - NOT_FOUND = 5 - ALREADY_EXISTS = 6 - PERMISSION_DENIED = 7 - UNAUTHENTICATED = 16 - RESOURCE_EXHAUSTED = 8 - FAILED_PRECONDITION = 9 - ABORTED = 10 - OUT_OF_RANGE = 11 - UNIMPLEMENTED = 12 - INTERNAL = 13 - UNAVAILABLE = 14 - DATA_LOSS = 15 +from ortools.util import status_pb2 +from ortools.util.python import status_streaming class InternalMathOptError(RuntimeError): @@ -55,7 +33,7 @@ class InternalMathOptError(RuntimeError): def status_proto_to_exception( - status_proto: rpc_pb2.StatusProto, + status_proto: status_pb2.StatusProto, ) -> Optional[Exception]: """Returns the Python exception that best match the input absl::Status. @@ -76,31 +54,11 @@ def status_proto_to_exception( Returns: The corresponding exception. None if the input status is OK. """ - try: - code = _StatusCode(status_proto.code) - except ValueError: - return InternalMathOptError( - f"unknown C++ error (code = {status_proto.code}):" - f" {status_proto.message}" - ) - - if code == _StatusCode.OK: + exception = status_streaming.status_proto_to_exception(status_proto) + if exception is None: return None - - # For expected errors we compute the corresponding class. - error_type: Optional[Type[Exception]] = None - if code == _StatusCode.INVALID_ARGUMENT: - error_type = ValueError - if code == _StatusCode.FAILED_PRECONDITION: - error_type = AssertionError - if code == _StatusCode.UNIMPLEMENTED: - error_type = NotImplementedError - if code == _StatusCode.INTERNAL: - error_type = InternalMathOptError - - if error_type is not None: - return error_type(f"{status_proto.message} (was C++ {code.name})") - - return InternalMathOptError( - f"unexpected C++ error {code.name}: {status_proto.message}" - ) + if isinstance(exception, status_streaming.InternalError): + return InternalMathOptError(str(exception)) + # Exception returned by status_streaming already inherit from the expected + # exceptions. + return exception diff --git a/ortools/math_opt/python/errors_test.py b/ortools/math_opt/python/errors_test.py index 0938bcbda23..713b0b36657 100644 --- a/ortools/math_opt/python/errors_test.py +++ b/ortools/math_opt/python/errors_test.py @@ -16,8 +16,9 @@ from absl.testing import absltest -from ortools.math_opt import rpc_pb2 from ortools.math_opt.python import errors +from ortools.util import status_pb2 +from ortools.util.python import status_streaming class StatusProtoToExceptionTest(absltest.TestCase): @@ -25,14 +26,15 @@ class StatusProtoToExceptionTest(absltest.TestCase): def test_ok(self) -> None: self.assertIsNone( errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=errors._StatusCode.OK.value) + status_pb2.StatusProto(code=status_streaming.StatusCode.OK.value) ) ) def test_invalid_argument(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.INVALID_ARGUMENT.value, message="something" + status_pb2.StatusProto( + code=status_streaming.StatusCode.INVALID_ARGUMENT.value, + message="something", ) ) self.assertIsInstance(error, ValueError) @@ -40,8 +42,8 @@ def test_invalid_argument(self) -> None: def test_failed_precondition(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.FAILED_PRECONDITION.value, + status_pb2.StatusProto( + code=status_streaming.StatusCode.FAILED_PRECONDITION.value, message="something", ) ) @@ -50,8 +52,9 @@ def test_failed_precondition(self) -> None: def test_unimplemented(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.UNIMPLEMENTED.value, message="something" + status_pb2.StatusProto( + code=status_streaming.StatusCode.UNIMPLEMENTED.value, + message="something", ) ) self.assertIsInstance(error, NotImplementedError) @@ -59,8 +62,8 @@ def test_unimplemented(self) -> None: def test_internal(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.INTERNAL.value, message="something" + status_pb2.StatusProto( + code=status_streaming.StatusCode.INTERNAL.value, message="something" ) ) self.assertIsInstance(error, errors.InternalMathOptError) @@ -68,21 +71,20 @@ def test_internal(self) -> None: def test_unexpected_code(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.DEADLINE_EXCEEDED.value, message="something" + status_pb2.StatusProto( + code=status_streaming.StatusCode.DEADLINE_EXCEEDED.value, + message="something", ) ) - self.assertIsInstance(error, errors.InternalMathOptError) - self.assertEqual( - str(error), "unexpected C++ error DEADLINE_EXCEEDED: something" - ) + self.assertIsInstance(error, status_streaming.DeadlineExceededError) + self.assertEqual(str(error), "something (was C++ DEADLINE_EXCEEDED)") def test_unknown_code(self) -> None: error = errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=-5, message="something") + status_pb2.StatusProto(code=-5, message="something") ) - self.assertIsInstance(error, errors.InternalMathOptError) - self.assertEqual(str(error), "unknown C++ error (code = -5): something") + self.assertIsInstance(error, status_streaming.UnexpectedCodeError) + self.assertEqual(str(error), "something (was C++ -5)") if __name__ == "__main__": diff --git a/ortools/math_opt/python/parameters.py b/ortools/math_opt/python/parameters.py index 061ccd2c406..96442f17728 100644 --- a/ortools/math_opt/python/parameters.py +++ b/ortools/math_opt/python/parameters.py @@ -32,52 +32,66 @@ class SolverType(enum.Enum): """The underlying solver to use. - This must stay synchronized with math_opt_parameters_pb2.SolverTypeProto. + This must stay synchronized with math_opt_parameters_pb2.SolverTypeProto. - Attributes: - GSCIP: Solving Constraint Integer Programs (SCIP) solver (third party). - Supports LP, MIP, and nonconvex integer quadratic problems. No dual data - for LPs is returned though. Prefer GLOP for LPs. - GUROBI: Gurobi solver (third party). Supports LP, MIP, and nonconvex integer - quadratic problems. Generally the fastest option, but has special - licensing, see go/gurobi-google for details. - GLOP: Google's Glop linear solver. Supports LP with primal and dual simplex - methods. - CP_SAT: Google's CP-SAT solver. Supports problems where all variables are - integer and bounded (or implied to be after presolve). Experimental - support to rescale and discretize problems with continuous variables. - MOE:begin_intracomment_strip - PDLP: Google's PDLP solver. Supports LP and convex diagonal quadratic - objectives. Uses first order methods rather than simplex. Can solve very - large problems. - MOE:end_intracomment_strip - GLPK: GNU Linear Programming Kit (GLPK) (third party). Supports MIP and LP. - Thread-safety: GLPK use thread-local storage for memory allocations. As a - consequence when using IncrementalSolver, the user must make sure that - instances are closed on the same thread as they are created or GLPK will - crash. To do so, use `with` or call IncrementalSolver#close(). It seems OK - to call IncrementalSolver#Solve() from another thread than the one used to - create the Solver but it is not documented by GLPK and should be avoided. - Of course these limitations do not apply to the solve() function that - recreates a new GLPK problem in the calling thread and destroys before - returning. When solving a LP with the presolver, a solution (and the - unbound rays) are only returned if an optimal solution has been found. - Else nothing is returned. See glpk-5.0/doc/glpk.pdf page #40 available - from glpk-5.0.tar.gz for details. - OSQP: The Operator Splitting Quadratic Program (OSQP) solver (third party). - Supports continuous problems with linear constraints and linear or convex - quadratic objectives. Uses a first-order method. - ECOS: The Embedded Conic Solver (ECOS) (third party). Supports LP and SOCP - problems. Uses interior point methods (barrier). - SCS: The Splitting Conic Solver (SCS) (third party). Supports LP and SOCP - problems. Uses a first-order method. - HIGHS: The HiGHS Solver (third party). Supports LP and MIP problems (convex - QPs are unimplemented). - SANTORINI: The Santorini Solver (first party). Supports MIP. Experimental, - do not use in production. - XPRESS: FICO Xpress solver (third party). Supports LP, MIP, and QP/MIQP - problems. Requires a local Xpress installation with XPRESSDIR set. - """ + Attributes: + GSCIP: Solving Constraint Integer Programs (SCIP) solver (third party). + Supports LP, MIP, and nonconvex integer quadratic problems. No dual data + for LPs is returned though. Prefer GLOP for LPs. + GUROBI: Gurobi solver (third party). Supports LP, MIP, and nonconvex integer + quadratic problems. Generally the fastest option, but has special + licensing, see go/gurobi-google for details. + GLOP: Google's Glop linear solver. Supports LP with primal and dual simplex + methods. + CP_SAT: Google's CP-SAT solver. Supports problems where all variables are + integer and bounded (or implied to be after presolve). Experimental + support to rescale and discretize problems with continuous variables. + MOE:begin_intracomment_strip + PDLP: Google's PDLP solver. Supports LP and convex diagonal quadratic + objectives. Uses first order methods rather than simplex. Can solve very + large problems. + MOE:end_intracomment_strip + GLPK: GNU Linear Programming Kit (GLPK) (third party). Supports MIP and LP. + Thread-safety: GLPK use thread-local storage for memory allocations. As a + consequence when using IncrementalSolver, the user must make sure that + instances are closed on the same thread as they are created or GLPK will + crash. To do so, use `with` or call IncrementalSolver#close(). It seems OK + to call IncrementalSolver#Solve() from another thread than the one used to + create the Solver but it is not documented by GLPK and should be avoided. + Of course these limitations do not apply to the solve() function that + recreates a new GLPK problem in the calling thread and destroys before + returning. When solving a LP with the presolver, a solution (and the + unbound rays) are only returned if an optimal solution has been found. + Else nothing is returned. See glpk-5.0/doc/glpk.pdf page #40 available + from glpk-5.0.tar.gz for details. + OSQP: The Operator Splitting Quadratic Program (OSQP) solver (third party). + Supports continuous problems with linear constraints and linear or convex + quadratic objectives. Uses a first-order method. + ECOS: The Embedded Conic Solver (ECOS) (third party). Supports LP and SOCP + problems. Uses interior point methods (barrier). + SCS: The Splitting Conic Solver (SCS) (third party). Supports LP and SOCP + problems. Uses a first-order method. + HIGHS: The HiGHS Solver (third party). Supports LP and MIP problems (convex + QPs are unimplemented). + SANTORINI: The Santorini Solver (first party). Supports MIP. Experimental, + do not use in production. + XPRESS: FICO Xpress solver (third party). Supports LP, MIP, and QP/MIQP + problems. Requires a local Xpress installation with XPRESSDIR set. + MIN_COST_FLOW: Google's Min-Cost Flow solver. Uses a specialized solver for + Min-Cost Flow problems (see + https://developers.google.com/optimization/flow/mincostflow). Supports LP + problems that match the structure of a Min-Cost Flow problem (see + go/mathopt-min-cost-flow). + Requirements: + * The constraint matrix must be the node-arc incidence matrix of a + digraph, that is, each variable appears in exactly two constraints, with + coefficients +1 and -1. + * Only linear constraints are allowed. + * All linear constraints must be equality constraints. + * All variable lower bounds must be 0. + * All variables and constraints must have integer bounds and costs. + * The objective must be linear. + """ # fmt: skip GSCIP = math_opt_parameters_pb2.SOLVER_TYPE_GSCIP GUROBI = math_opt_parameters_pb2.SOLVER_TYPE_GUROBI @@ -91,6 +105,7 @@ class SolverType(enum.Enum): HIGHS = math_opt_parameters_pb2.SOLVER_TYPE_HIGHS SANTORINI = math_opt_parameters_pb2.SOLVER_TYPE_SANTORINI XPRESS = math_opt_parameters_pb2.SOLVER_TYPE_XPRESS + MIN_COST_FLOW = math_opt_parameters_pb2.SOLVER_TYPE_MIN_COST_FLOW def solver_type_from_proto( diff --git a/ortools/math_opt/python/solve.py b/ortools/math_opt/python/solve.py index 695cdcfc561..328069642ca 100644 --- a/ortools/math_opt/python/solve.py +++ b/ortools/math_opt/python/solve.py @@ -19,13 +19,14 @@ from pybind11_abseil.status import StatusNotOk -from ortools.math_opt import parameters_pb2, rpc_pb2 +from ortools.math_opt import parameters_pb2 from ortools.math_opt.core.python import solver from ortools.math_opt.python import (callback, compute_infeasible_subsystem_result, errors, init_arguments, message_callback, model, model_parameters, parameters, result) +from ortools.util import status_pb2 from ortools.util.python import solve_interrupter SolveCallback = Callable[[callback.CallbackData], callback.CallbackResult] @@ -317,7 +318,7 @@ def _status_not_ok_to_exception(err: StatusNotOk) -> Exception: The corresponding exception. """ ret = errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=err.canonical_code, message=err.message) + status_pb2.StatusProto(code=err.canonical_code, message=err.message) ) # We never expect StatusNotOk to be OK. assert ret is not None, err diff --git a/ortools/math_opt/python/solve_test.py b/ortools/math_opt/python/solve_test.py index 6aa8a846e9e..8aa95b6329b 100644 --- a/ortools/math_opt/python/solve_test.py +++ b/ortools/math_opt/python/solve_test.py @@ -71,6 +71,31 @@ def test_solve_error(self) -> None: with self.assertRaisesRegex(ValueError, "variables.*lower_bound > upper_bound"): solve.solve(mod, parameters.SolverType.GLOP) + def test_min_cost_flow_solve(self) -> None: + mod = model.Model() + ab = mod.add_variable(lb=0.0, ub=11.0, is_integer=False) + bc = mod.add_variable(lb=0.0, ub=12.0, is_integer=False) + + mod.add_linear_constraint(ab == 10.0) + mod.add_linear_constraint(bc - ab == 0.0) + mod.add_linear_constraint(-bc == -10.0) + + mod.minimize(2.0 * ab + 3.0 * bc) + + res = solve.solve(mod, parameters.SolverType.MIN_COST_FLOW) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(50.0, res.termination.objective_bounds.primal_bound) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual(50.0, res.solutions[0].primal_solution.objective_value) + self._assert_dict_almost_equal( + {ab: 10.0, bc: 10.0}, res.solutions[0].primal_solution.variable_values + ) + def test_lp_solve(self) -> None: mod = model.Model(name="test_model") # Solve the problem: diff --git a/ortools/math_opt/python/variables.py b/ortools/math_opt/python/variables.py index c89b1e7c2a6..e859f31e721 100644 --- a/ortools/math_opt/python/variables.py +++ b/ortools/math_opt/python/variables.py @@ -26,7 +26,7 @@ from ortools.math_opt.elemental.python import enums from ortools.math_opt.python import bounded_expressions, from_model -from ortools.math_opt.python.elemental import elemental +from ortools.math_opt.python.elemental import elemental as elemental_lib LinearTypes = Union[int, float, "LinearBase"] QuadraticTypes = Union[int, float, "LinearBase", "QuadraticBase"] @@ -604,11 +604,11 @@ class is simply a reference to that data. Do not create a Variable directly, __slots__ = "_elemental", "_id" - def __init__(self, elem: elemental.Elemental, vid: int) -> None: + def __init__(self, elem: elemental_lib.Elemental, vid: int) -> None: """Internal only, prefer Model functions (add_variable() and get_variable()).""" if not isinstance(vid, int): raise TypeError(f"vid type should be int, was:{type(vid)}") - self._elemental: elemental.Elemental = elem + self._elemental: elemental_lib.Elemental = elem self._id: int = vid @property @@ -656,7 +656,7 @@ def id(self) -> int: return self._id @property - def elemental(self) -> elemental.Elemental: + def elemental(self) -> elemental_lib.Elemental: """Internal use only.""" return self._elemental diff --git a/ortools/math_opt/result.proto b/ortools/math_opt/result.proto index 016c41c09f0..981d63b239a 100644 --- a/ortools/math_opt/result.proto +++ b/ortools/math_opt/result.proto @@ -17,9 +17,9 @@ syntax = "proto3"; package operations_research.math_opt; import "google/protobuf/duration.proto"; +import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/pdlp/solve_log.proto"; import "ortools/math_opt/solution.proto"; -import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/math_opt/solvers/osqp.proto"; option java_package = "com.google.ortools.mathopt"; diff --git a/ortools/math_opt/rpc.proto b/ortools/math_opt/rpc.proto index 74bd75b553d..947bd2eeff0 100644 --- a/ortools/math_opt/rpc.proto +++ b/ortools/math_opt/rpc.proto @@ -15,6 +15,7 @@ syntax = "proto3"; package operations_research.math_opt; +import "ortools/util/status.proto"; import "ortools/math_opt/callback.proto"; import "ortools/math_opt/infeasible_subsystem.proto"; import "ortools/math_opt/model.proto"; @@ -140,12 +141,3 @@ message ComputeInfeasibleSubsystemResponse { // messages for solvers that support message callbacks. repeated string messages = 2; } - -// 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/math_opt/samples/cpp/BUILD.bazel b/ortools/math_opt/samples/cpp/BUILD.bazel index e2ba07f0803..344bb4e4e6b 100644 --- a/ortools/math_opt/samples/cpp/BUILD.bazel +++ b/ortools/math_opt/samples/cpp/BUILD.bazel @@ -11,7 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@pip_deps//:requirements.bzl", "requirement") load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("//tools/testing:bintest.bzl", "py_bintest") cc_binary( name = "basic_example", @@ -279,3 +281,65 @@ cc_binary( "@abseil-cpp//absl/status", ], ) + +cc_binary( + name = "min_cost_flow", + srcs = ["min_cost_flow.cc"], + tags = [ + "not_build:arm", + "not_run:arm", + ], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:glop_solver", + "//ortools/math_opt/solvers:min_cost_flow_solver", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/status", + ], +) + +py_bintest( + name = "min_cost_flow_test", + srcs = ["min_cost_flow_test.py"], + named_data = {"bin": ":min_cost_flow"}, + tags = [ + "not_build:arm", + "not_run:arm", + ], + deps = [requirement("absl-py")], +) + +cc_binary( + name = "matching_by_min_cost_flow", + srcs = ["matching_by_min_cost_flow.cc"], + tags = [ + "not_build:arm", + "not_run:arm", + ], + deps = [ + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solvers:glop_solver", + "//ortools/math_opt/solvers:min_cost_flow_solver", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/time", + ], +) + +py_bintest( + name = "matching_by_min_cost_flow_test", + srcs = ["matching_by_min_cost_flow_test.py"], + named_data = {"bin": ":matching_by_min_cost_flow"}, + tags = [ + "not_build:arm", + "not_run:arm", + ], + deps = [requirement("absl-py")], +) diff --git a/ortools/math_opt/samples/cpp/matching_by_min_cost_flow.cc b/ortools/math_opt/samples/cpp/matching_by_min_cost_flow.cc new file mode 100644 index 00000000000..b054b30605a --- /dev/null +++ b/ortools/math_opt/samples/cpp/matching_by_min_cost_flow.cc @@ -0,0 +1,172 @@ +// 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 + +#include "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/random/random.h" +#include "absl/status/status.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(operations_research::math_opt::SolverType, solver, + operations_research::math_opt::SolverType::kMinCostFlow, + "What underlying LP or min cost flow solver to use."); +ABSL_FLAG(bool, gurobi_network, false, + "Used only when solver is Gurobi, request network simplex (may be " + "ignored)."); + +ABSL_FLAG(int, nodes, 10'000, + "The number of nodes for each side of random bipartite input graph."); + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +// We encode a bipartite matching problem as a min cost flow problem. +// +// Problem data for bipartite matching on a (multi-)graph: +// +// * B = (L, R, E): a bipartite (multi-)graph, where |L| = |R| and which is +// REQUIRED to have a perfect matching. +// * c_e: the cost of using edge e. +// +// The goal is to find the perfect matching (a set of arcs that touch each node +// exactly once) at minimum cost. We allow for B to be a multigraph (you can +// have multip edges connecting any (l, r) pair) as this does not change the +// difficulty of the problem. +// +// We let adj(n) for n in L cup R be the arcs adjacent to node n (with one +// endpoint equal to n). +// +// The typical way to encode this in LP (on decision variable x_e) is: +// +// min sum_{e in E} c_e x_e +// s.t. sum_{e adj(l)} x_e = 1 for all l in L +// sum_{e adj(r)} x_e = 1 for all r in R +// 0 <= x_e <= 1. +// +// (Note that the LP is integral, so any optimal basic solution will have x_e +// in {0, 1} and thus be a matching.) +// +// However, for min-cost-flow detection to work, we must have each variable +// appear in the constraint matrix once with coefficient +1 and once with +// coefficient -1. So instead, we encode the problem such that every L node has +// a net supply of +1, every node in R has a net supply of -1, and the edges are +// directed arcs from L to R. Effectively, we multiply the constraints for r in +// R by -1, arriving at the formulation: +// +// min sum_{e in E} c_e x_e +// s.t. sum_{e adj(l)} x_e = 1 for all l in L +// sum_{e adj(r)} -x_e = -1 for all r in R +// 0 <= x_e <= 1. +// +// which is now in min-cost-flow form (and this problem is still integral). +// +// In the example below, we generate our bipartite multi-graph as a function of +// a single parameter n (from FLAGS_nodes) as follows: +// * n nodes in L +// * n nodes in R +// * 5*n edges with endpoints uniform on each side (repeat endpoints OK), each +// with cost Uniform(0,100) and integer. +// * An edge from i in L to i in R wth cost 100 (to ensure a perfect matching +// exists). +// +// So a solution with cost 100 * num_nodes is always possible, and typically +// we can find a much better solution. +absl::Status Main() { + absl::Time start = absl::Now(); + std::cout << "Generating random matching problem." << std::endl; + const int num_nodes_per_side = absl::GetFlag(FLAGS_nodes); + std::mt19937 bitgen; + std::vector> edges; + const int max_cost = 100; + for (int i = 0; i < 5 * num_nodes_per_side; ++i) { + const int left = absl::Uniform(bitgen, 0, num_nodes_per_side); + const int right = absl::Uniform(bitgen, 0, num_nodes_per_side); + const int cost = absl::Uniform(bitgen, 0, max_cost); + edges.push_back({left, right, cost}); + } + // ensure always feasible + for (int i = 0; i < num_nodes_per_side; ++i) { + const int left = i; + const int right = i; + const int cost = max_cost; + edges.push_back({left, right, cost}); + } + std::vector left_cons; + std::vector right_cons; + std::cout << "Done generating problem (" << absl::Now() - start << ")" + << std::endl; + start = absl::Now(); + std::cout << "Building MathOpt model." << std::endl; + math_opt::Model model; + for (int i = 0; i < num_nodes_per_side; ++i) { + math_opt::LinearConstraint left_c = model.AddLinearConstraint(); + model.set_lower_bound(left_c, 1.0); + model.set_upper_bound(left_c, 1.0); + left_cons.push_back(left_c); + + math_opt::LinearConstraint right_c = model.AddLinearConstraint(); + model.set_lower_bound(right_c, -1.0); + model.set_upper_bound(right_c, -1.0); + right_cons.push_back(right_c); + } + + math_opt::LinearExpression obj; + for (const auto [left, right, cost] : edges) { + math_opt::Variable edge_var = model.AddContinuousVariable(0.0, 1.0); + model.set_coefficient(left_cons[left], edge_var, 1.0); + model.set_coefficient(right_cons[right], edge_var, -1.0); + obj += cost * edge_var; + } + model.Minimize(obj); + std::cout << "Done building model (" << absl::Now() - start << ")" + << std::endl; + start = absl::Now(); + std::cout << "Solving matching problem." << std::endl; + + const math_opt::SolverType solver_type = absl::GetFlag(FLAGS_solver); + math_opt::SolveParameters params; + params.enable_output = true; + if (solver_type == math_opt::SolverType::kGurobi && + absl::GetFlag(FLAGS_gurobi_network)) { + params.gurobi.param_values["NetworkAlg"] = "1"; + } + OR_ASSIGN_OR_RETURN( + const math_opt::SolveResult result, + Solve(model, solver_type, {.parameters = std::move(params)})); + std::cout << "Done solving (" << absl::Now() - start << ")" << std::endl; + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + std::cout << "Cost: " << result.objective_value() << std::endl; + return absl::OkStatus(); +} +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/cpp/matching_by_min_cost_flow_test.py b/ortools/math_opt/samples/cpp/matching_by_min_cost_flow_test.py new file mode 100644 index 00000000000..f537578591f --- /dev/null +++ b/ortools/math_opt/samples/cpp/matching_by_min_cost_flow_test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# 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. + +from absl.testing import absltest + +from tools.testing import binary_test + + +class MatchingByMinCostFlowTest(binary_test.BinaryTestCase): + + def test_finds_solution(self) -> None: + output = self.assert_binary_succeeds("$(bin)", "--nodes=30") + cost = self.assert_has_line_with_prefixed_number("Cost:", output) + self.assertLessEqual(cost, 30 * 100.0) + self.assertGreaterEqual(cost, 0.0) + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/math_opt/samples/cpp/min_cost_flow.cc b/ortools/math_opt/samples/cpp/min_cost_flow.cc new file mode 100644 index 00000000000..8d6a18c76ef --- /dev/null +++ b/ortools/math_opt/samples/cpp/min_cost_flow.cc @@ -0,0 +1,146 @@ +// 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 "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "ortools/base/init_google.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/cpp/math_opt.h" + +ABSL_FLAG(operations_research::math_opt::SolverType, solver, + operations_research::math_opt::SolverType::kMinCostFlow, + "What underlying LP or min cost flow solver to use."); +ABSL_FLAG(bool, gurobi_network, false, + "Used only when solver is Gurobi, request network simplex (may be " + "ignored)."); + +namespace { + +namespace math_opt = ::operations_research::math_opt; + +// We build and solve the min cost flow problem with nodes: +// +// Node | Net Supply +// s | 5 +// a | 0 +// b | 0 +// t | -5 +// +// (above, negative supply is demand, so we are routing 5 units of flow from s +// to t), and with arcs: +// +// Source | Dest | Cost | Capacity +// s | a | 3 | 5 +// s | b | 5 | 5 +// a | t | 0 | 2 +// b | t | 0 | 5 +// +// This graph has two s-t paths, s -> a -> t and s -> b -> t. The first path is +// cheaper, but we can only send 2 units of flow on it, so we send the other +// three units on the second path. +// +// In mathopt, we can detect min-cost-flow and run a faster algorithm when: +// * There are only linear constraints +// * Each variable appears in two constraints, with constraint coefficient of 1 +// once and -1 once. +// * The variable lower bounds are zero, and the variable upper bounds are +// nonnegative integers, and the variables are continuous. +// * The objective coefficients are nonnegative integers. +// * All linear constraints are equality constraints with integer RHS. +// +// See go/mathopt-min-cost-flow for more details. +// +// In such a formulation, the variables are the flow along an arc, and the +// linear constraints are flow conservation at each node. The LP is of the form: +// +// Data: +// * G = (N, A): a directed graph +// * s_n: the net supply of node n in N +// * c_a: the cost of arc a +// * u_a: the capacity of arc a +// +// Decision variables: +// * x_a: flow on arc a. +// +// min sum_{a in A} c_a x_a +// s.t. sum_{a out of n} x_a - sum_{a into n} x_a = s_n for all n in N +// 0 <= x_a <= u_a +// +// So for this particular min cost flow problem, our model (on decision +// variables sa, sb, at, bt) is: +// +// min 3sa + 5sb +// s.t. sa + sb = 5 (flow conservation s) +// at - sa = 0 (flow conservation a) +// bt - sb = 0 (flow conservation b) +// -at - bt = -5 (flow conservation t) +// 0 <= sa <= 5 +// 0 <= sb <= 5 +// 0 <= at <= 2 +// 0 <= bt <= 5 +// +// We expect an optimal solution of: sa = at = 2, sb = bt = 3, cost 21. +absl::Status Main() { + math_opt::Model model; + const math_opt::Variable sa = model.AddContinuousVariable(0.0, 5.0); + const math_opt::Variable sb = model.AddContinuousVariable(0.0, 5.0); + const math_opt::Variable at = model.AddContinuousVariable(0.0, 2.0); + const math_opt::Variable bt = model.AddContinuousVariable(0.0, 5.0); + model.Minimize(3.0 * sa + 5.0 * sb); + model.AddLinearConstraint(sa + sb == 5.0); + model.AddLinearConstraint(at - sa == 0.0); + model.AddLinearConstraint(bt - sb == 0.0); + model.AddLinearConstraint(-at - bt == -5.0); + + const math_opt::SolverType solver_type = absl::GetFlag(FLAGS_solver); + math_opt::SolveParameters params; + params.enable_output = true; + if (solver_type == math_opt::SolverType::kGurobi && + absl::GetFlag(FLAGS_gurobi_network)) { + // This problem is so small that Gurobi can solve it in presolve, which + // prevents network simplex from running. If we disable presolve, we can + // see in the logs "Starting network simplex...", indicating that network + // simplex actually ran. + // + // On a real problem, you should typically not disable presolve when you + // have network structure as we do below. + params.presolve = math_opt::Emphasis::kOff; + params.gurobi.param_values["NetworkAlg"] = "1"; + } + OR_ASSIGN_OR_RETURN( + const math_opt::SolveResult result, + Solve(model, solver_type, {.parameters = std::move(params)})); + OR_RETURN_IF_ERROR(result.termination.EnsureIsOptimal()); + std::cout << "Objective value: " << result.objective_value() << std::endl; + std::cout << "sa: " << result.variable_values().at(sa) << std::endl; + std::cout << "sb: " << result.variable_values().at(sb) << std::endl; + std::cout << "at: " << result.variable_values().at(at) << std::endl; + std::cout << "bt: " << result.variable_values().at(bt) << std::endl; + return absl::OkStatus(); +} + +} // namespace + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + const absl::Status status = Main(); + if (!status.ok()) { + LOG(QFATAL) << status; + } + return 0; +} diff --git a/ortools/math_opt/samples/cpp/min_cost_flow_test.py b/ortools/math_opt/samples/cpp/min_cost_flow_test.py new file mode 100644 index 00000000000..986fb787f9e --- /dev/null +++ b/ortools/math_opt/samples/cpp/min_cost_flow_test.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# 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. + +from absl.testing import absltest + +from tools.testing import binary_test + + +class MinCostFlowTest(binary_test.BinaryTestCase): + + def test_finds_solution(self) -> None: + output = self.assert_binary_succeeds("$(bin)") + cost = self.assert_has_line_with_prefixed_number("Objective value:", output) + self.assertAlmostEqual(cost, 21.0, delta=1e-5) + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/math_opt/solver_tests/BUILD.bazel b/ortools/math_opt/solver_tests/BUILD.bazel index 53babae92d6..9fec87f3745 100644 --- a/ortools/math_opt/solver_tests/BUILD.bazel +++ b/ortools/math_opt/solver_tests/BUILD.bazel @@ -59,6 +59,7 @@ cc_library( "@abseil-cpp//absl/strings", "@abseil-cpp//absl/synchronization", "@abseil-cpp//absl/types:span", + "@rules_cc//cc/runfiles", ], # Make sure the tests are included when using --dynamic_mode=off. alwayslink = 1, @@ -84,6 +85,7 @@ cc_library( "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/time", + "@rules_cc//cc/runfiles", ], # Make sure the tests are included when using --dynamic_mode=off. alwayslink = 1, @@ -109,6 +111,26 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "min_cost_flow_tests", + testonly = 1, + srcs = ["min_cost_flow_tests.cc"], + hdrs = ["min_cost_flow_tests.h"], + deps = [ + "//ortools/base:gmock", + "//ortools/math_opt/cpp:matchers", + "//ortools/math_opt/cpp:math_opt", + "//ortools/port:proto_utils", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/strings", + ], + # Make sure the tests are included when using --dynamic_mode=off. + alwayslink = 1, +) + cc_library( name = "lp_incomplete_solve_tests", testonly = 1, @@ -136,6 +158,7 @@ cc_library( hdrs = ["invalid_input_tests.h"], deps = [ ":base_solver_test", + ":test_models", "//ortools/base:gmock", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_update_cc_proto", @@ -301,6 +324,7 @@ cc_library( "@abseil-cpp//absl/strings", "@abseil-cpp//absl/time", "@abseil-cpp//absl/types:span", + "@rules_cc//cc/runfiles", ], # Make sure the tests are included when using --dynamic_mode=off. alwayslink = 1, @@ -329,6 +353,7 @@ cc_library( "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/time", + "@rules_cc//cc/runfiles", ], # Make sure the tests are included when using --dynamic_mode=off. alwayslink = 1, @@ -438,6 +463,8 @@ cc_test( "//ortools/math_opt/solvers:cp_sat_solver", "//ortools/math_opt/solvers:glop_solver", "//ortools/math_opt/solvers:gscip_solver", + "//ortools/math_opt/solvers:min_cost_flow_solver", + "//ortools/math_opt/testing:stream", ], ) diff --git a/ortools/math_opt/solver_tests/CMakeLists.txt b/ortools/math_opt/solver_tests/CMakeLists.txt index f71a1885c1c..3a12580706e 100644 --- a/ortools/math_opt/solver_tests/CMakeLists.txt +++ b/ortools/math_opt/solver_tests/CMakeLists.txt @@ -80,6 +80,25 @@ ortools_cxx_library( TESTING ) +ortools_cxx_library( + NAME + ${_PREFIX}_min_cost_flow_tests + SOURCES + "min_cost_flow_tests.cc" + "min_cost_flow_tests.h" + TYPE + STATIC + LINK_LIBRARIES + ortools::base_gmock + absl::log + absl::status + absl::status_matchers + absl::strings + ortools::math_opt_matchers + ortools::math_opt_base_solver_test + TESTING +) + ortools_cxx_library( NAME ${_PREFIX}_lp_incomplete_solve_tests diff --git a/ortools/math_opt/solver_tests/callback_tests.cc b/ortools/math_opt/solver_tests/callback_tests.cc index 53d75c45d0d..fe2e95fcbd4 100644 --- a/ortools/math_opt/solver_tests/callback_tests.cc +++ b/ortools/math_opt/solver_tests/callback_tests.cc @@ -75,19 +75,23 @@ std::ostream& operator<<(std::ostream& out, } CallbackTestParams::CallbackTestParams( - const SolverType solver_type, const bool integer_variables, + const SolverType solver_type, const TestModelClass model_class, const bool add_lazy_constraints, const bool add_cuts, absl::flat_hash_set supported_events, std::optional all_solutions, std::optional reaches_cut_callback) : solver_type(solver_type), - integer_variables(integer_variables), + model_class(model_class), add_lazy_constraints(add_lazy_constraints), add_cuts(add_cuts), supported_events(std::move(supported_events)), all_solutions(std::move(all_solutions)), reaches_cut_callback(std::move(reaches_cut_callback)) {} +bool CallbackTestParams::uses_integer_variables() const { + return model_class == TestModelClass::kIp; +} + namespace { template @@ -110,7 +114,7 @@ absl::StatusOr> LoadMiplibInstance( std::ostream& operator<<(std::ostream& out, const CallbackTestParams& params) { out << "{ solver_type: " << params.solver_type - << ", integer_variables: " << params.integer_variables + << ", model_class: " << ToString(params.model_class) << ", add_lazy_constraints: " << params.add_lazy_constraints << ", add_cuts: " << params.add_cuts << ", supported_events: " << absl::StrJoin(params.supported_events, ",", @@ -164,6 +168,10 @@ TEST_P(MessageCallbackTest, EmptyIfNotSupported) { } TEST_P(MessageCallbackTest, ObjectiveValueAndEndingSubstring) { + if (GetParam().solver_type == SolverType::kMinCostFlow) { + GTEST_SKIP() << "MinCostFlow solver does not report objective value or " + "ending substring."; + } Model model("model"); const Variable x = model.AddVariable(0, 21.0, GetParam().integer_variables, "x"); @@ -291,8 +299,10 @@ TEST_P(CallbackTest, EventPresolve) { } Model model("model"); - Variable x = model.AddVariable(0, 2.0, GetParam().integer_variables, "x"); - Variable y = model.AddVariable(0, 3.0, GetParam().integer_variables, "y"); + Variable x = + model.AddVariable(0, 2.0, GetParam().uses_integer_variables(), "x"); + Variable y = + model.AddVariable(0, 3.0, GetParam().uses_integer_variables(), "y"); model.AddLinearConstraint(y <= 1.0); model.Maximize(2.0 * x + y); SolveArguments args = { @@ -375,7 +385,7 @@ TEST_P(CallbackTest, EventBarrier) { // Make a model that requires multiple barrier steps to solve. const std::unique_ptr model = - SmallModel(GetParam().integer_variables); + SmallModel(GetParam().uses_integer_variables()); SolveArguments args; args.parameters.presolve = Emphasis::kOff; @@ -404,7 +414,7 @@ TEST_P(CallbackTest, EventSolutionAlwaysCalled) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); Model model("model"); const Variable x = model.AddBinaryVariable("x"); @@ -450,7 +460,7 @@ TEST_P(CallbackTest, EventSolutionInterrupt) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); // A model where we will not prove optimality immediately. const std::unique_ptr model = @@ -479,7 +489,7 @@ TEST_P(CallbackTest, EventSolutionCalledMoreThanOnce) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); Model model("model"); const Variable x = model.AddBinaryVariable("x"); @@ -539,7 +549,7 @@ TEST_P(CallbackTest, EventSolutionLazyConstraint) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); Model model("model"); const Variable x = model.AddBinaryVariable("x"); @@ -587,7 +597,7 @@ TEST_P(CallbackTest, EventSolutionLazyConstraintWithLinearConstraints) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); Model model("model"); const Variable x = model.AddBinaryVariable("x"); @@ -633,7 +643,7 @@ TEST_P(CallbackTest, EventSolutionFilter) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); Model model("model"); const Variable x = model.AddBinaryVariable("x"); @@ -686,7 +696,7 @@ TEST_P(CallbackTest, EventNodeCut) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); // Max sum_i x_i // s.t. x_i + x_j + x_k <= 2 for all i < j < k @@ -759,7 +769,7 @@ TEST_P(CallbackTest, EventNodeFilter) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); // Use the MIPLIB instance 23588, which has optimal solution 8090 and LP // relaxation of 7649.87. This instance was selected because every // supported solver can solve it quickly (a few seconds), but no solver can @@ -801,7 +811,7 @@ TEST_P(CallbackTest, EventMip) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); // Use the MIPLIB instance 23588, which has optimal solution 8090 and LP // relaxation of 7649.87. This instance was selected because every @@ -846,7 +856,7 @@ TEST_P(CallbackTest, StatusPropagation) { } // This test must use integer variables. - ASSERT_TRUE(GetParam().integer_variables); + ASSERT_TRUE(GetParam().uses_integer_variables()); // Check status propagation by adding an invalid cut. Model model("model"); @@ -875,8 +885,8 @@ TEST_P(CallbackTest, StatusPropagation) { } TEST_P(CallbackTest, UnsupportedEvents) { - Model model("model"); - model.AddVariable(0, 1.0, GetParam().integer_variables, "x"); + const std::unique_ptr model = + MinimalModelForTestModelClass(GetParam().model_class); for (const CallbackEvent event : Enum::AllValues()) { if (GetParam().supported_events.contains(event)) { @@ -889,7 +899,7 @@ TEST_P(CallbackTest, UnsupportedEvents) { .callback = [](const CallbackData&) { return CallbackResult{}; }}; EXPECT_THAT( - Solve(model, GetParam().solver_type, args), + Solve(*model, GetParam().solver_type, args), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr(CallbackEventProto_Name(EnumToProto(event))))); } diff --git a/ortools/math_opt/solver_tests/callback_tests.h b/ortools/math_opt/solver_tests/callback_tests.h index e6bf6fe2ed9..70b7506e29b 100644 --- a/ortools/math_opt/solver_tests/callback_tests.h +++ b/ortools/math_opt/solver_tests/callback_tests.h @@ -25,6 +25,7 @@ #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/solver_tests/base_solver_test.h" +#include "ortools/math_opt/solver_tests/test_models.h" namespace operations_research { namespace math_opt { @@ -77,7 +78,7 @@ class MessageCallbackTest // Parameters for CallbackTest. struct CallbackTestParams { - CallbackTestParams(SolverType solver_type, bool integer_variables, + CallbackTestParams(SolverType solver_type, TestModelClass model_class, bool add_lazy_constraints, bool add_cuts, absl::flat_hash_set supported_events, std::optional all_solutions, @@ -86,8 +87,8 @@ struct CallbackTestParams { // The solver to test. SolverType solver_type; - // True if the tests should be performed with integer variables. - bool integer_variables; + // The model class to use for tests. + TestModelClass model_class; // If the solver supports adding lazy constraints at the MIP_SOLUTION event. bool add_lazy_constraints; @@ -107,6 +108,9 @@ struct CallbackTestParams { // result in the test on adding cuts at event kMipNode not running. std::optional reaches_cut_callback; + // Returns true if model_class uses integer variables (i.e., is `kIp`). + bool uses_integer_variables() const; + friend std::ostream& operator<<(std::ostream& out, const CallbackTestParams& params); }; diff --git a/ortools/math_opt/solver_tests/generic_tests.cc b/ortools/math_opt/solver_tests/generic_tests.cc index 635b085daaf..87bac082499 100644 --- a/ortools/math_opt/solver_tests/generic_tests.cc +++ b/ortools/math_opt/solver_tests/generic_tests.cc @@ -24,6 +24,7 @@ #include #include +#include "absl/base/nullability.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/log/log.h" @@ -35,6 +36,7 @@ #include "absl/time/time.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" +#include "ortools/base/types.h" #include "ortools/gurobi/gurobi_stdout_matchers.h" #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/cpp/matchers.h" @@ -55,12 +57,12 @@ std::ostream& operator<<(std::ostream& out, GenericTestParameters::GenericTestParameters(const SolverType solver_type, const bool support_interrupter, - const bool integer_variables, + const TestModelClass model_class, std::string expected_log, SolveParameters solve_parameters) : solver_type(solver_type), support_interrupter(support_interrupter), - integer_variables(integer_variables), + model_class(model_class), expected_log(std::move(expected_log)), solve_parameters(std::move(solve_parameters)) {} @@ -68,8 +70,8 @@ std::ostream& operator<<(std::ostream& out, const GenericTestParameters& params) { out << "{ solver_type: " << params.solver_type << ", support_interrupter: " << params.support_interrupter - << ", integer_variables: " << params.integer_variables - << ", expected_log: \"" << absl::CEscape(params.expected_log) << "\"" + << ", model_class: " << params.model_class << ", expected_log: \"" + << absl::CEscape(params.expected_log) << "\"" << ", solve_parameters: " << ProtobufShortDebugString(params.solve_parameters.Proto()) << " }"; return out; @@ -82,39 +84,6 @@ using ::testing::status::IsOkAndHolds; constexpr double kInf = std::numeric_limits::infinity(); -TEST_P(GenericTest, EmptyModel) { - Model model; - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); -} - -TEST_P(GenericTest, OffsetOnlyMinimization) { - Model model; - model.Minimize(4.0); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(4.0))); -} - -TEST_P(GenericTest, OffsetOnlyMaximization) { - Model model; - model.Maximize(4.0); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(4.0))); -} - -TEST_P(GenericTest, OffsetMinimization) { - Model model; - const Variable x = - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, "x"); - model.Minimize(2 * x + 4); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(2.0))); -} - -TEST_P(GenericTest, OffsetMaximization) { - Model model; - const Variable x = - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, "x"); - model.Maximize(2 * x + 4); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(8.0))); -} - TEST_P(GenericTest, SolveTime) { // We use a non-trivial problem since on WASM the time resolution is of 1ms // and thus a trivial model could be solved in absl::ZeroDuration(). @@ -125,13 +94,13 @@ TEST_P(GenericTest, SolveTime) { // too long solve times. Here we just want to make sure that we have a long // enough solve time so that it is not too close to zero. constexpr int kMinN = 10; - constexpr int kMaxN = 30; - constexpr int kIncrementN = 5; + constexpr int kMaxN = 210; + constexpr int kIncrementN = 50; constexpr absl::Duration kMinSolveTime = absl::Milliseconds(5); for (int n = kMinN; n <= kMaxN; n += kIncrementN) { SCOPED_TRACE(absl::StrCat("n = ", n)); const std::unique_ptr model = - DenseIndependentSet(GetParam().integer_variables, /*n=*/n); + NontrivialModel(GetParam().model_class, n); const absl::Time start = absl::Now(); ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(*model)); @@ -154,7 +123,10 @@ TEST_P(GenericTest, InterruptBeforeSolve) { GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test."; } - const std::unique_ptr model = SmallModel(GetParam().integer_variables); + // GLPK as a MIP solver terminates before checking the interrupt when the + // model is too simple, so we cannot use MinimalModelForTestModelClass(). + const std::unique_ptr model = + NontrivialModel(GetParam().model_class, 5); SolveInterrupter interrupter; interrupter.Interrupt(); @@ -173,7 +145,8 @@ TEST_P(GenericTest, InterruptAfterSolve) { GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test."; } - const std::unique_ptr model = SmallModel(GetParam().integer_variables); + const std::unique_ptr model = + MinimalModelForTestModelClass(GetParam().model_class); SolveInterrupter interrupter; @@ -199,7 +172,8 @@ TEST_P(GenericTest, InterrupterNeverTriggered) { GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test."; } - const std::unique_ptr model = SmallModel(GetParam().integer_variables); + const std::unique_ptr model = + MinimalModelForTestModelClass(GetParam().model_class); SolveInterrupter interrupter; @@ -209,7 +183,8 @@ TEST_P(GenericTest, InterrupterNeverTriggered) { ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(*model, GetParam().solver_type, args)); - EXPECT_THAT(result, IsOptimal()); + EXPECT_THAT(result, + IsOptimal(kMinimalModelForTestModelClassOptimalObjective)); } TEST_P(GenericTest, NoStdoutOutputByDefault) { @@ -217,31 +192,26 @@ TEST_P(GenericTest, NoStdoutOutputByDefault) { GTEST_SKIP() << "Stdout can't be captured."; } - Model model("model"); - const Variable x = - model.AddVariable(0, 21.0, GetParam().integer_variables, "x"); - model.Maximize(2.0 * x); - + const std::unique_ptr model = + MinimalModelForTestModelClass(GetParam().model_class); ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); - ASSERT_OK(SimpleSolve(model)); + ASSERT_OK(SimpleSolve(*model)); EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), EmptyOrGurobiLicenseWarningIfGurobi( /*is_gurobi=*/GetParam().solver_type == SolverType::kGurobi)); } TEST_P(GenericTest, EnableOutputPrintsToStdOut) { - Model model("model"); - const Variable x = - model.AddVariable(0, 21.0, GetParam().integer_variables, "x"); - model.Maximize(2.0 * x); - + const std::unique_ptr model = + MinimalModelForTestModelClass(GetParam().model_class); SolveParameters params = GetParam().solve_parameters; params.enable_output = true; ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout); - EXPECT_THAT(Solve(model, GetParam().solver_type, {.parameters = params}), - IsOkAndHolds(IsOptimal(42.0))); + EXPECT_THAT( + Solve(*model, GetParam().solver_type, {.parameters = params}), + IsOkAndHolds(IsOptimal(kMinimalModelForTestModelClassOptimalObjective))); if (ScopedStdStreamCapture::kIsSupported) { EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(), @@ -268,100 +238,114 @@ std::string AllNonAsciiCharacters() { return oss.str(); } -TEST_P(GenericTest, ModelNameTooLong) { - // GLPK and Gurobi have a limit for problem name to 255 characters; here we - // use long names to validate that it does not raise any assertion (along with - // other solvers). - EXPECT_THAT(SimpleSolve(Model(std::string(1024, 'x'))), - IsOkAndHolds(IsOptimal(0.0))); - - // GLPK refuses control characters (iscntrl()) in the problem name and has a - // limit for problem name to 255 characters. Here we validate that the - // truncation of the string takes into account the quoting of the control - // characters (we pass all 7-bits ASCII characters to make sure they are - // accepted). - EXPECT_THAT(SimpleSolve(Model(AllAsciiCharacters() + std::string(1024, 'x'))), - IsOkAndHolds(IsOptimal(0.0))); - - // GLPK should accept non-ASCII characters (>= 0x80). - EXPECT_THAT( - SimpleSolve(Model(AllNonAsciiCharacters() + std::string(1024, 'x'))), - IsOkAndHolds(IsOptimal(0.0))); +std::vector HardNames() { + return { + // GLPK and Gurobi have a limit for problem name to 255 characters; here + // we use long names to validate that it does not raise any assertion + // (along with other solvers). + std::string(1024, 'x'), + // GLPK refuses control characters (iscntrl()) in the problem name and has + // a limit for problem name to 255 characters. Here we validate that the + // truncation of the string takes into account the quoting of the control + // characters (we pass all 7-bits ASCII characters to make sure they are + // accepted). + AllAsciiCharacters() + std::string(1024, 'x'), + // GLPK should accept non-ASCII characters (>= 0x80). + AllNonAsciiCharacters() + std::string(1024, 'x')}; } -TEST_P(GenericTest, VariableNames) { - // See rationales in ModelName for these tests. - { - Model model; - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, - std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); +TEST_P(GenericTest, HardModelName) { + const std::vector hard_names = HardNames(); + for (int i = 0; i < hard_names.size(); ++i) { + SCOPED_TRACE(i); + EXPECT_THAT(SimpleSolve(Model(hard_names[i])), + IsOkAndHolds(IsOptimal(0.0))); } - { - Model model; - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, - AllAsciiCharacters() + std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); - } - { - Model model; - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, - AllNonAsciiCharacters() + std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); +} + +// Returns a Model that: +// * Is within `model_class` +// * Has variables with each name in `var_names` +// * Has optimal objective value zero. +absl_nonnull std::unique_ptr ModelWithNamedVars( + const TestModelClass model_class, + const std::vector& var_names) { + auto result = std::make_unique(); + const bool integer = model_class == TestModelClass::kIp; + for (const std::string& var_name : var_names) { + const math_opt::Variable st = + result->AddVariable(0.0, 1.0, integer, var_name); + if (model_class == TestModelClass::kMinCostFlow) { + result->AddLinearConstraint(st == 1); + result->AddLinearConstraint(-st == -1); + } } - // Test two variables that thanks to the truncation will get the same name are - // not an issue for the solver. - { - Model model; - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, - std::string(1024, '-') + 'x'); - model.AddVariable(-1.0, 2.0, GetParam().integer_variables, - std::string(1024, '-') + 'y'); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); + return result; +} + +TEST_P(GenericTest, HardVariableNames) { + const std::vector hard_names = HardNames(); + for (int i = 0; i < hard_names.size(); ++i) { + SCOPED_TRACE(i); + std::unique_ptr model = + ModelWithNamedVars(GetParam().model_class, {hard_names[i]}); + EXPECT_THAT(SimpleSolve(*model), IsOkAndHolds(IsOptimal(0.0))); + // Test two variables that thanks to the truncation will get the same name + // are not an issue for the solver. + std::unique_ptr repeat_names = ModelWithNamedVars( + GetParam().model_class, {hard_names[i], hard_names[i] + "xyz"}); + EXPECT_THAT(SimpleSolve(*repeat_names), IsOkAndHolds(IsOptimal(0.0))); } } -TEST_P(GenericTest, LinearConstraintNames) { - // See rationales in ModelName for these tests. - { - Model model; - model.AddLinearConstraint(-1.0, 2.0, std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); +// Returns a Model that: +// * Is within every TestModelClass +// * Has linear constraints with each name in `lin_con_names` +// * Has optimal objective value zero. +absl_nonnull std::unique_ptr ModelWithNamedLinearConstraints( + std::vector lin_con_names) { + auto result = std::make_unique(); + for (const std::string& n : lin_con_names) { + result->AddLinearConstraint(0.0, 0.0, n); } - { - Model model; - model.AddLinearConstraint(-1.0, 2.0, - AllAsciiCharacters() + std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); + return result; +} + +TEST_P(GenericTest, HardLinearConstraintNames) { + if (GetParam().solver_type == SolverType::kGlpk && + GetParam().solve_parameters.lp_algorithm == LPAlgorithm::kBarrier) { + GTEST_SKIP() << "Bug in GLPK barrier, it errors on this problem w/o names."; } - { - Model model; - model.AddLinearConstraint(-1.0, 2.0, - AllNonAsciiCharacters() + std::string(1024, 'x')); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); + if (GetParam().solver_type == SolverType::kEcos) { + GTEST_SKIP() << "ECOS solver crashes on this test (segfault)."; } - // Test two constraints that thanks to the truncation will get the same name - // are not an issue for the solver. - { - Model model; - model.AddLinearConstraint(-1.0, 2.0, std::string(1024, '-') + 'x'); - model.AddLinearConstraint(-1.0, 2.0, std::string(1024, '-') + 'y'); - EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0))); + const std::vector hard_names = HardNames(); + for (int i = 0; i < hard_names.size(); ++i) { + SCOPED_TRACE(i); + std::unique_ptr model = + ModelWithNamedLinearConstraints({hard_names[i]}); + EXPECT_THAT(SimpleSolve(*model), IsOkAndHolds(IsOptimal(0.0))); + // Test two linear constraints that thanks to the truncation will get the + // same name are not an issue for the solver. + std::unique_ptr repeat_names = + ModelWithNamedLinearConstraints({hard_names[i], hard_names[i] + "xyz"}); + EXPECT_THAT(SimpleSolve(*repeat_names), IsOkAndHolds(IsOptimal(0.0))); } +} + +TEST_P(GenericTest, NamesUnset) { // Solvers should accept a ModelProto whose linear_constraints.names repeated // field is not set. As of 2023-08-21 this is done by remove_names. { - Model model; - const Variable x = - model.AddVariable(0.0, 1.0, GetParam().integer_variables, "x"); - model.AddLinearConstraint(x == 1.0, "c"); + auto model = MinimalModelForTestModelClass(GetParam().model_class); SolverInitArguments init_args; init_args.remove_names = true; ASSERT_OK_AND_ASSIGN( const SolveResult result, - Solve(model, GetParam().solver_type, + Solve(*model, GetParam().solver_type, {.parameters = GetParam().solve_parameters}, init_args)); - EXPECT_THAT(result, IsOptimal(0.0)); + EXPECT_THAT(result, + IsOptimal(kMinimalModelForTestModelClassOptimalObjective)); } } @@ -370,11 +354,16 @@ TEST_P(GenericTest, LinearConstraintNames) { // Test that the solvers properly translates the MathOpt ids to their internal // indices by using a model where indices don't start a zero. TEST_P(GenericTest, NonZeroIndices) { + if (GetParam().solver_type == SolverType::kMinCostFlow) { + GTEST_SKIP() + << "MinCostFlow solver is incompatible with this test (b/496243723)"; + } + const bool integer_variables = GetParam().model_class == TestModelClass::kIp; // To test that solvers don't truncate by mistake numbers in the whole range // of valid id numbers, we force the use of the maximum value by using a input // model proto. ModelProto base_model_proto; - constexpr int64_t kMaxValidId = std::numeric_limits::max() - 1; + constexpr int64_t kMaxValidId = kint64max - 1; { VariablesProto& variables = *base_model_proto.mutable_variables(); variables.add_ids(kMaxValidId - 1); @@ -398,8 +387,7 @@ TEST_P(GenericTest, NonZeroIndices) { model->DeleteVariable(model->Variables().back()); model->DeleteLinearConstraint(model->LinearConstraints().back()); - const Variable x = - model->AddVariable(0.0, kInf, GetParam().integer_variables, "x"); + const Variable x = model->AddVariable(0.0, kInf, integer_variables, "x"); EXPECT_EQ(x.id(), kMaxValidId); model->Maximize(x); @@ -419,6 +407,11 @@ testing::Matcher> StatusIsInvertedBounds( } TEST_P(GenericTest, InvertedVariableBounds) { + if (GetParam().solver_type == SolverType::kMinCostFlow) { + GTEST_SKIP() + << "MinCostFlow solver is incompatible with this test (b/496264107)"; + } + const bool integer_variables = GetParam().model_class == TestModelClass::kIp; const SolveArguments solve_args = {.parameters = GetParam().solve_parameters}; // First test with bounds inverted at the construction of the solver. @@ -442,7 +435,7 @@ TEST_P(GenericTest, InvertedVariableBounds) { } const Variable x = model.AddVariable(/*lower_bound=*/lb, /*upper_bound=*/ub, - GetParam().integer_variables, "x"); + integer_variables, "x"); ASSERT_EQ(x.id(), kXId); model.Maximize(3.0 * x); @@ -480,9 +473,9 @@ TEST_P(GenericTest, InvertedVariableBounds) { model.DeleteVariable(model.AddVariable()); } - const Variable x = model.AddVariable(/*lower_bound=*/initial_lb, - /*upper_bound=*/initial_ub, - GetParam().integer_variables, "x"); + const Variable x = + model.AddVariable(/*lower_bound=*/initial_lb, + /*upper_bound=*/initial_ub, integer_variables, "x"); ASSERT_EQ(x.id(), kXId); model.Maximize(3.0 * x); @@ -528,9 +521,8 @@ TEST_P(GenericTest, InvertedVariableBounds) { model.DeleteVariable(model.AddVariable()); } - const Variable x = - model.AddVariable(/*lower_bound=*/3.0, /*upper_bound=*/4.0, - GetParam().integer_variables, "x"); + const Variable x = model.AddVariable( + /*lower_bound=*/3.0, /*upper_bound=*/4.0, integer_variables, "x"); ASSERT_EQ(x.id(), kXId); model.Maximize(3.0 * x); @@ -544,7 +536,7 @@ TEST_P(GenericTest, InvertedVariableBounds) { // Test the update using a new variable with inverted bounds (in case the // update code path is not identical to the NewIncrementalSolver() one). const Variable y = model.AddVariable(/*lower_bound=*/lb, /*upper_bound=*/ub, - GetParam().integer_variables, "y"); + integer_variables, "y"); model.Maximize(3.0 * x + y); ASSERT_OK(solver->Update()); EXPECT_THAT(solver->SolveWithoutUpdate(solve_args), @@ -553,14 +545,18 @@ TEST_P(GenericTest, InvertedVariableBounds) { } TEST_P(GenericTest, InvertedLinearConstraintBounds) { + if (GetParam().solver_type == SolverType::kMinCostFlow) { + GTEST_SKIP() + << "MinCostFlow solver is incompatible with this test (b/496264107)"; + } + const bool integer_variables = GetParam().model_class == TestModelClass::kIp; const SolveArguments solve_args = {.parameters = GetParam().solve_parameters}; // First test with bounds inverted at the construction of the solver. { Model model; - const Variable x = - model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0, - GetParam().integer_variables, "x"); + const Variable x = model.AddVariable( + /*lower_bound=*/0.0, /*upper_bound=*/10.0, integer_variables, "x"); // Here we add some constraints that we immediately remove so that the id of // `u` below won't be 0. This will help making sure bugs in conversion from @@ -587,9 +583,8 @@ TEST_P(GenericTest, InvertedLinearConstraintBounds) { // Then test with bounds inverted during an update. { Model model; - const Variable x = - model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0, - GetParam().integer_variables, "x"); + const Variable x = model.AddVariable( + /*lower_bound=*/0.0, /*upper_bound=*/10.0, integer_variables, "x"); // Here we add some constraints that we immediately remove so that the id of // `u` below won't be 0. This will help making sure bugs in conversion from @@ -622,9 +617,8 @@ TEST_P(GenericTest, InvertedLinearConstraintBounds) { // Finally test with an update adding a constraint with inverted bounds. { Model model; - const Variable x = - model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0, - GetParam().integer_variables, "x"); + const Variable x = model.AddVariable( + /*lower_bound=*/0.0, /*upper_bound=*/10.0, integer_variables, "x"); // Here we add some constraints that we immediately remove so that the id of // `u` below won't be 0. This will help making sure bugs in conversion from diff --git a/ortools/math_opt/solver_tests/generic_tests.h b/ortools/math_opt/solver_tests/generic_tests.h index 0ff8dea419a..934618c8927 100644 --- a/ortools/math_opt/solver_tests/generic_tests.h +++ b/ortools/math_opt/solver_tests/generic_tests.h @@ -20,16 +20,18 @@ #include #include +#include "absl/base/attributes.h" #include "absl/status/statusor.h" #include "gtest/gtest.h" #include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/solver_tests/test_models.h" namespace operations_research { namespace math_opt { struct GenericTestParameters { GenericTestParameters(SolverType solver_type, bool support_interrupter, - bool integer_variables, std::string expected_log, + TestModelClass model_class, std::string expected_log, SolveParameters solve_parameters = {}); // The tested solver. @@ -38,8 +40,8 @@ struct GenericTestParameters { // True if the solver support SolveInterrupter. bool support_interrupter; - // True if the tests should be performed with integer variables. - bool integer_variables; + // What type of model to run the solver on. + TestModelClass model_class; // A message included in the solver logs when an optimal solution is found. std::string expected_log; @@ -81,7 +83,7 @@ struct TimeLimitTestParameters { // The tested solver. SolverType solver_type; - // The test problem will be a 0-1 IP if true, otherwise will be an LP. + // Should we use integer variables or continuous ones. bool integer_variables; // A supported callback event, or nullopt if no event is supported. @@ -97,6 +99,9 @@ struct TimeLimitTestParameters { // will create either a small LP or IP, depending on the bool // integer_variables below. // +// NOTE: min cost flow solver is not supported for this suite, as there are no +// time limits or callbacks supported yet. +// // To use these tests, in file _test.cc write: // INSTANTIATE_TEST_SUITE_P(TimeLimitTest, TimeLimitTest, // testing::Values(TimeLimitTestParameters( diff --git a/ortools/math_opt/solver_tests/invalid_input_tests.cc b/ortools/math_opt/solver_tests/invalid_input_tests.cc index 3c4ce8d0739..caafb8fba29 100644 --- a/ortools/math_opt/solver_tests/invalid_input_tests.cc +++ b/ortools/math_opt/solver_tests/invalid_input_tests.cc @@ -27,6 +27,7 @@ #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/model.pb.h" #include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/port/proto_utils.h" namespace operations_research { @@ -39,19 +40,19 @@ using ::testing::status::StatusIs; std::ostream& operator<<(std::ostream& out, const InvalidInputTestParameters& params) { out << "{ solver_type: " << params.solver_type - << " use_integer_variables: " << params.use_integer_variables << " }"; + << " model_class: " << ToString(params.model_class) << " }"; return out; } InvalidParameterTest::InvalidParameterTest() - : model_(), x_(model_.AddContinuousVariable(0.0, 1.0, "x")) { - model_.Maximize(2 * x_); -} + : model_(MinimalModelForTestModelClass(GetParam().model_class)) {} InvalidParameterTestParams::InvalidParameterTestParams( - const SolverType solver_type, SolveParameters solve_parameters, + const SolverType solver_type, TestModelClass model_class, + SolveParameters solve_parameters, std::vector expected_error_substrings) : solver_type(solver_type), + model_class(model_class), solve_parameters(std::move(solve_parameters)), expected_error_substrings(std::move(expected_error_substrings)) {} @@ -73,7 +74,7 @@ TEST_P(InvalidInputTest, InvalidModel) { model.mutable_variables()->add_lower_bounds(2.0); model.mutable_variables()->add_upper_bounds(3.0); model.mutable_variables()->add_upper_bounds(4.0); - model.mutable_variables()->add_integers(GetParam().use_integer_variables); + model.mutable_variables()->add_integers(GetParam().uses_integer_variables()); model.mutable_variables()->add_names("x3"); EXPECT_THAT(Solver::New(EnumToProto(TestedSolver()), model, /*arguments=*/{}), @@ -96,7 +97,7 @@ TEST_P(InvalidInputTest, InvalidUpdate) { model.mutable_variables()->add_ids(3); model.mutable_variables()->add_lower_bounds(2.0); model.mutable_variables()->add_upper_bounds(3.0); - model.mutable_variables()->add_integers(GetParam().use_integer_variables); + model.mutable_variables()->add_integers(GetParam().uses_integer_variables()); model.mutable_variables()->add_names("x3"); ASSERT_OK_AND_ASSIGN(auto solver, diff --git a/ortools/math_opt/solver_tests/invalid_input_tests.h b/ortools/math_opt/solver_tests/invalid_input_tests.h index 7036de6bd01..bf878277cc0 100644 --- a/ortools/math_opt/solver_tests/invalid_input_tests.h +++ b/ortools/math_opt/solver_tests/invalid_input_tests.h @@ -25,6 +25,7 @@ #include "ortools/math_opt/cpp/math_opt.h" #include "ortools/math_opt/parameters.pb.h" #include "ortools/math_opt/solver_tests/base_solver_test.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/storage/model_storage.h" namespace operations_research { @@ -32,12 +33,16 @@ namespace math_opt { struct InvalidInputTestParameters { SolverType solver_type; - bool use_integer_variables; + TestModelClass model_class; InvalidInputTestParameters(const SolverType solver_type, - const bool use_integer_variables) - : solver_type(solver_type), - use_integer_variables(use_integer_variables) {} + const TestModelClass model_class) + : solver_type(solver_type), model_class(model_class) {} + + // Returns true if model_class uses integer variables (i.e., is `kIp`). + bool uses_integer_variables() const { + return model_class == TestModelClass::kIp; + } friend std::ostream& operator<<(std::ostream& out, const InvalidInputTestParameters& params); @@ -47,7 +52,7 @@ struct InvalidInputTestParameters { // INSTANTIATE_TEST_SUITE_P( // InvalidInputTest, InvalidInputTest, // testing::Values(InvalidInputTestParameters( -// SolverType::k, /*use_integer_variables=*/true))); +// SolverType::k, TestModelClass::kIp))); // TODO(b/172553545): this test should not be repeated for each solver since it // tests that the Solver class validates the model before calling the // interface. @@ -59,10 +64,12 @@ class InvalidInputTest struct InvalidParameterTestParams { InvalidParameterTestParams( - SolverType solver_type, SolveParameters solve_parameters, + SolverType solver_type, TestModelClass model_class, + SolveParameters solve_parameters, std::vector expected_error_substrings); SolverType solver_type; + TestModelClass model_class; SolveParameters solve_parameters; std::vector expected_error_substrings; @@ -77,11 +84,10 @@ class InvalidParameterTest absl::StatusOr SimpleSolve( const SolveParameters& parameters = GetParam().solve_parameters) { - return Solve(model_, GetParam().solver_type, {.parameters = parameters}); + return Solve(*model_, GetParam().solver_type, {.parameters = parameters}); } - Model model_; - const Variable x_; + const std::unique_ptr model_; }; } // namespace math_opt diff --git a/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc b/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc index d57ee2be69e..e30ddf16407 100644 --- a/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc +++ b/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc @@ -292,12 +292,6 @@ TEST_P(BranchPrioritiesTest, PrioritiesAreSetProperly) { // See PrioritiesAreSetProperly for details on the model and solve parameters. TEST_P(BranchPrioritiesTest, PrioritiesClearedAfterIncrementalSolve) { - if (GetParam().solver_type == SolverType::kXpress) { - // This test does not work with Xpress since Xpress does not clear/reset - // model parameters after a solve. See the comment in XpressSolver::Solve - // in xpress_solver.cc. - GTEST_SKIP() << "Xpress does not clear model parameters in Solve()."; - } Model model; Variable x = model.AddContinuousVariable(-3.0, 1.0, "x"); Variable y = model.AddContinuousVariable(0.0, 3.0, "y"); diff --git a/ortools/math_opt/solver_tests/logical_constraint_tests.cc b/ortools/math_opt/solver_tests/logical_constraint_tests.cc index e3654c7ff57..886558e5846 100644 --- a/ortools/math_opt/solver_tests/logical_constraint_tests.cc +++ b/ortools/math_opt/solver_tests/logical_constraint_tests.cc @@ -36,8 +36,7 @@ LogicalConstraintTestParameters::LogicalConstraintTestParameters( const bool supports_incremental_add_and_deletes, const bool supports_incremental_variable_deletions, const bool supports_deleting_indicator_variables, - const bool supports_updating_binary_variables, - const bool supports_sos_on_expressions) + const bool supports_updating_binary_variables) : solver_type(solver_type), parameters(std::move(parameters)), supports_integer_variables(supports_integer_variables), @@ -50,8 +49,7 @@ LogicalConstraintTestParameters::LogicalConstraintTestParameters( supports_incremental_variable_deletions), supports_deleting_indicator_variables( supports_deleting_indicator_variables), - supports_updating_binary_variables(supports_updating_binary_variables), - supports_sos_on_expressions(supports_sos_on_expressions) {} + supports_updating_binary_variables(supports_updating_binary_variables) {} std::ostream& operator<<(std::ostream& out, const LogicalConstraintTestParameters& params) { @@ -70,9 +68,7 @@ std::ostream& operator<<(std::ostream& out, << ", supports_deleting_indicator_variables: " << (params.supports_deleting_indicator_variables ? "true" : "false") << ", supports_updating_binary_variables: " - << (params.supports_updating_binary_variables ? "true" : "false") - << ", supports_sos_on_expressions: " - << (params.supports_sos_on_expressions ? "true" : "false") << " }"; + << (params.supports_updating_binary_variables ? "true" : "false") << " }"; return out; } @@ -90,19 +86,11 @@ constexpr absl::string_view no_sos2_support_message = constexpr absl::string_view no_indicator_support_message = "This test is disabled as the solver does not support indicator " "constraints"; -constexpr absl::string_view no_updating_binary_variables_message = - "This test is disabled as the solver does not support updating " - "binary variables"; -constexpr absl::string_view no_deleting_indicator_variables_message = - "This test is disabled as the solver does not support deleting " - "indicator variables"; -constexpr absl::string_view no_incremental_add_and_deletes_message = - "This test is disabled as the solver does not support incremental " - "add/delete"; // We test SOS1 constraints with both explicit weights and default weights. TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) { - if (!GetParam().supports_sos_on_expressions) { + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 GTEST_SKIP() << "skipped since SOS on expressions are not supported"; } Model model; @@ -121,7 +109,8 @@ TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) { // We test SOS2 constraints with both explicit weights and default weights. TEST_P(SimpleLogicalConstraintTest, CanBuildSos2Model) { - if (!GetParam().supports_sos_on_expressions) { + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 GTEST_SKIP() << "skipped since SOS on expressions are not supported"; } Model model; @@ -303,9 +292,6 @@ TEST_P(SimpleLogicalConstraintTest, Sos2VariableInMultipleTerms) { // The optimal solution for the modified problem is (x*, y*) = (0, 1) with // objective value 2. TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) { - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << no_incremental_add_and_deletes_message; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -357,9 +343,6 @@ TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) { // The optimal solution for the modified problem is (x*, y*, z*) = (0, 1, 1) // with objective value 4. TEST_P(IncrementalLogicalConstraintTest, LinearToSos2Update) { - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << no_incremental_add_and_deletes_message; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -887,9 +870,6 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorConstraint) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << no_incremental_add_and_deletes_message; - } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -933,9 +913,6 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << no_incremental_add_and_deletes_message; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable indicator = model.AddBinaryVariable("indicator"); @@ -980,9 +957,6 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorVariable) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_deleting_indicator_variables) { - GTEST_SKIP() << no_deleting_indicator_variables_message; - } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -1063,9 +1037,6 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_updating_binary_variables) { - GTEST_SKIP() << no_updating_binary_variables_message; - } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -1113,9 +1084,6 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateChangesIndicatorVariableBound) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_updating_binary_variables) { - GTEST_SKIP() << no_updating_binary_variables_message; - } Model model; const Variable x = model.AddIntegerVariable(0.0, 1.0, "x"); const Variable y = model.AddIntegerVariable(0.0, 1.0, "y"); @@ -1180,9 +1148,6 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } - if (!GetParam().supports_updating_binary_variables) { - GTEST_SKIP() << no_updating_binary_variables_message; - } Model model; const Variable x = model.AddIntegerVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); diff --git a/ortools/math_opt/solver_tests/logical_constraint_tests.h b/ortools/math_opt/solver_tests/logical_constraint_tests.h index 923e631dbe7..87f15e363dd 100644 --- a/ortools/math_opt/solver_tests/logical_constraint_tests.h +++ b/ortools/math_opt/solver_tests/logical_constraint_tests.h @@ -30,8 +30,7 @@ struct LogicalConstraintTestParameters { bool supports_incremental_add_and_deletes, bool supports_incremental_variable_deletions, bool supports_deleting_indicator_variables, - bool supports_updating_binary_variables, - bool supports_sos_on_expressions = true); + bool supports_updating_binary_variables); // The tested solver. SolverType solver_type; @@ -63,10 +62,6 @@ struct LogicalConstraintTestParameters { // True if the solver supports updates (changing bounds or vartype) to binary // variables. bool supports_updating_binary_variables; - - // True if the solver supports SOS constraints on expressions. False if - // SOS constraints are only supported on singleton variables. - bool supports_sos_on_expressions; }; std::ostream& operator<<(std::ostream& out, @@ -74,6 +69,10 @@ std::ostream& operator<<(std::ostream& out, // A suite of unit tests for logical constraints. // +// These tests assume that the solver supports optimizing linear programs (LPs) +// or mixed-integer programs (MIPs), depending on if variables are continuous +// or integer. +// // To use these tests, in file _test.cc, write: // INSTANTIATE_TEST_SUITE_P( // SimpleLogicalConstraintTest, SimpleLogicalConstraint, @@ -97,6 +96,10 @@ class SimpleLogicalConstraintTest // A suite of unit tests for logical constraints. // +// These tests assume that the solver supports optimizing linear programs (LPs) +// or mixed-integer programs (MIPs), depending on if variables are continuous +// or integer. +// // To use these tests, in file _test.cc, write: // INSTANTIATE_TEST_SUITE_P( // IncrementalLogicalConstraintTest, IncrementalLogicalConstraint, diff --git a/ortools/math_opt/solver_tests/lp_tests.cc b/ortools/math_opt/solver_tests/lp_tests.cc index f26e5cd93ff..62cdb2f32e6 100644 --- a/ortools/math_opt/solver_tests/lp_tests.cc +++ b/ortools/math_opt/solver_tests/lp_tests.cc @@ -118,6 +118,102 @@ TEST_P(SimpleLpTest, ProtoNonIncrementalSolve) { } } +TEST_P(SimpleLpTest, EmptyModel) { + Model model; + ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model)); + EXPECT_THAT(result, IsOptimalWithSolution(0.0, {})); + // Highs doesn't return a dual solution for offset only problems for unknown + // reasons. + if (GetParam().supports_duals && + GetParam().solver_type != SolverType::kHighs) { + EXPECT_THAT(result, IsOptimalWithDualSolution(0.0, {}, {})); + } +} + +TEST_P(SimpleLpTest, OffsetOnlyMinimization) { + Model model; + model.Minimize(4.0); + ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model)); + EXPECT_THAT(result, IsOptimalWithSolution(4.0, {})); + // Highs doesn't return a dual solution for offset only problems for unknown + // reasons. + if (GetParam().supports_duals && + GetParam().solver_type != SolverType::kHighs) { + EXPECT_THAT(result, IsOptimalWithDualSolution(4.0, {}, {})); + } +} + +TEST_P(SimpleLpTest, OffsetOnlyMaximization) { + Model model; + model.Maximize(4.0); + ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model)); + EXPECT_THAT(result, IsOptimalWithSolution(4.0, {})); + // Highs doesn't return a dual solution for offset only problems for unknown + // reasons. + if (GetParam().supports_duals && + GetParam().solver_type != SolverType::kHighs) { + EXPECT_THAT(result, IsOptimalWithDualSolution(4.0, {}, {})); + } +} + +// Primal: +// min 2x + 4 +// x >= -1 +// +// Dual: +// max -r + 4 +// s.t. r <= 2 +// +// (r is the reduced cost for x). +// +// Optimal solution: +// obj = 2 +// x* = -1 +// r* = 2 +TEST_P(SimpleLpTest, OffsetMinimization) { + if (GetParam().solver_type == SolverType::kGlpk && + GetParam().parameters.lp_algorithm == LPAlgorithm::kBarrier) { + GTEST_SKIP() << "glpk interior point buggy, errors on this problem"; + } + Model model; + const Variable x = model.AddContinuousVariable(-1.0, kInf, "x"); + model.Minimize(2 * x + 4); + ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model)); + EXPECT_THAT(result, IsOptimalWithSolution(2.0, {{x, -1.0}})); + if (GetParam().supports_duals) { + EXPECT_THAT(result, IsOptimalWithDualSolution(2.0, {}, {{x, 2.0}})); + } +} + +// Primal: +// max 2x + 4 +// x <= 2 +// +// Dual: +// min 2r + 4 +// s.t. r >= 2 +// +// (r is the reduced cost for x). +// +// Optimal solution: +// obj = 8 +// x* = 2 +// r* = 2 +TEST_P(SimpleLpTest, OffsetMaximization) { + if (GetParam().solver_type == SolverType::kGlpk && + GetParam().parameters.lp_algorithm == LPAlgorithm::kBarrier) { + GTEST_SKIP() << "glpk interior point buggy, errors on this problem"; + } + Model model; + const Variable x = model.AddContinuousVariable(-kInf, 2.0, "x"); + model.Maximize(2 * x + 4); + ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model)); + EXPECT_THAT(result, IsOptimalWithSolution(8.0, {{x, 2.0}})); + if (GetParam().supports_duals) { + EXPECT_THAT(result, IsOptimalWithDualSolution(8.0, {}, {{x, 2.0}})); + } +} + // TODO(b/184447031): change descriptions to avoid d(y, r)/d_max(y,r) and // go/mathopt-doc-math#dual diff --git a/ortools/math_opt/solver_tests/min_cost_flow_tests.cc b/ortools/math_opt/solver_tests/min_cost_flow_tests.cc new file mode 100644 index 00000000000..d91b0e7a575 --- /dev/null +++ b/ortools/math_opt/solver_tests/min_cost_flow_tests.cc @@ -0,0 +1,689 @@ +// 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/math_opt/solver_tests/min_cost_flow_tests.h" + +#include +#include +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/port/proto_utils.h" + +namespace operations_research::math_opt { + +constexpr double kInf = std::numeric_limits::infinity(); + +std::ostream& operator<<(std::ostream& out, + const MinCostFlowTestParams& params) { + out << "name: " << params.name << " solver_type: " << params.solver_type + << " default_parameters: " + << ProtobufShortDebugString(params.default_parameters.Proto()); + + if (params.lp_not_flow_error_substring.has_value()) { + out << " lp_not_flow_error_substring: '" + << absl::CEscape(*params.lp_not_flow_error_substring) << "'"; + } + + if (params.mip_not_flow_error_substring.has_value()) { + out << " mip_not_flow_error_substring: '" + << absl::CEscape(*params.mip_not_flow_error_substring) << "'"; + } + + if (params.floating_point_cost_error_substring.has_value()) { + out << " floating_point_cost_error_substring: '" + << absl::CEscape(*params.floating_point_cost_error_substring) << "'"; + } + + if (params.floating_point_capacity_error_substring.has_value()) { + out << " floating_point_capacity_error_substring: '" + << absl::CEscape(*params.floating_point_capacity_error_substring) + << "'"; + } + out << " certifies_nontrivial_infeasibility " + << params.certifies_nontrivial_infeasibility + << " request_dual_rays_params: " + << ProtobufShortDebugString(params.request_dual_rays_params.Proto()) + << " returns_dual_solution: " << params.returns_dual_solution; + return out; +} + +namespace { + +using ::testing::HasSubstr; +using ::testing::UnorderedElementsAre; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +TEST_P(MinCostFlowTest, MinimizeEmptyModel) { + Model model; + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {}))); +} + +TEST_P(MinCostFlowTest, MaximizeEmptyModel) { + Model model; + model.set_maximize(); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {}))); +} + +TEST_P(MinCostFlowTest, MinimizeOffset) { + Model model; + model.Minimize(3.0); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(3.0, {}))); +} + +TEST_P(MinCostFlowTest, MaximizeOffset) { + Model model; + model.Maximize(3.0); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(3.0, {}))); +} + +TEST_P(MinCostFlowTest, MinimalOptimal) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.0, {{st, 2.0}}))); +} + +TEST_P(MinCostFlowTest, MinimalInfeasible) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 1.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(TerminatesWithOneOf( + {TerminationReason::kInfeasible, + TerminationReason::kInfeasibleOrUnbounded}))); +} + +TEST_P(MinCostFlowTest, InfiniteCapacity) { + Model model; + const Variable st = + model.AddContinuousVariable(0.0, std::numeric_limits::infinity()); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.0, {{st, 2.0}}))); +} + +TEST_P(MinCostFlowTest, HasOffset) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st + 11.0); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(17.0, {{st, 2.0}}))); +} + +TEST_P(MinCostFlowTest, ObjectiveDirection) { + for (const bool is_maximize : {false, true}) { + SCOPED_TRACE(absl::StrCat("is_maximize: ", is_maximize)); + Model model; + const Variable st1 = model.AddContinuousVariable(0.0, kInf); + const Variable st2 = model.AddContinuousVariable(0.0, kInf); + model.AddLinearConstraint(st1 + st2 == 2.0); // flow out of s + model.AddLinearConstraint(-st1 - st2 == -2.0); // flow into t + model.set_is_maximize(is_maximize); + model.set_objective_coefficient(st1, 3.0); + model.set_objective_coefficient(st2, 4.0); + + double expected_obj; + VariableMap expected_solution; + if (is_maximize) { + expected_obj = 8.0; + expected_solution = {{st1, 0.0}, {st2, 2.0}}; + } else { + expected_obj = 6.0; + expected_solution = {{st1, 2.0}, {st2, 0.0}}; + } + EXPECT_THAT( + Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(expected_obj, expected_solution))); + } +} + +TEST_P(MinCostFlowTest, Unbalanced) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, kInf); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -3.0); // flow into t + model.Minimize(st); + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(TerminatesWithOneOf( + {TerminationReason::kInfeasible, + TerminationReason::kInfeasibleOrUnbounded}))); +} + +TEST_P(MinCostFlowTest, LpNotFlowMissingDest) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 0.0); // flow out of s + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowMissingSource) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(-st == 0.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowMultipleSources) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 0.0); // flow out of s + model.AddLinearConstraint(-st == 0.0); // flow into t + model.AddLinearConstraint(st == 0.0); // extra source + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowMultipleDestinations) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 0.0); // flow out of s + model.AddLinearConstraint(-st == 0.0); // flow into t + model.AddLinearConstraint(-st == 0.0); // extra dest + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowAbsCoefficientsNotOne) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(2.0 * st == 0.0); // flow out of s + model.AddLinearConstraint(-3.0 * st == 0.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowConstraintAtleast) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st >= 0.0); // flow out of s + model.AddLinearConstraint(st == 0.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowConstraintAtMost) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 0.0); // flow out of s + model.AddLinearConstraint(st <= 0.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowConstraintRange) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 0.0); // flow out of s + model.AddLinearConstraint(-4.0 <= st <= 0.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(0.0, {{st, 0.0}}))); + } +} + +TEST_P(MinCostFlowTest, LpNotFlowVariableLbNotZero) { + Model model; + const Variable st = model.AddContinuousVariable(2.0, 5.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(st == 2.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().lp_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().lp_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.0, {{st, 2.0}}))); + } +} + +TEST_P(MinCostFlowTest, MipNotFlow) { + Model model; + const Variable st = model.AddIntegerVariable(0.0, 5.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + if (GetParam().mip_not_flow_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().mip_not_flow_error_substring))); + } else { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.0, {{st, 2.0}}))); + } +} + +TEST_P(MinCostFlowTest, FloatingPointCost) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + model.AddLinearConstraint(st == 2.0); // flow out of s + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.2 * st); + if (!GetParam().floating_point_cost_error_substring.has_value()) { + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.4, {{st, 2.0}}))); + } else { + EXPECT_THAT( + Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().floating_point_cost_error_substring))); + } +} + +TEST_P(MinCostFlowTest, FloatingPointCapacity) { + Model model; + const Variable st1 = model.AddContinuousVariable(0.0, 0.5); + const Variable st2 = model.AddContinuousVariable(0.0, 1.5); + model.AddLinearConstraint(st1 + st2 == 2.0); // flow out of s + model.AddLinearConstraint(-st1 - st2 == -2.0); // flow into t + model.Minimize(3 * st1 + 3 * st2); + if (!GetParam().floating_point_capacity_error_substring.has_value()) { + EXPECT_THAT( + Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(6.0, {{st1, 0.5}, {st2, 1.5}}))); + } else { + EXPECT_THAT( + Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr(*GetParam().floating_point_capacity_error_substring))); + } +} + +// Primal: +// min 3st +// s.t st = 2 (s) +// -st = -2 (t) +// st in [0,1] (r) +// +// Dual: +// max 2s - 2t + r +// s.t. s - t + r <= 3 +// r <= 0 +// +// Has the basic dual rays: +// * (s, t, r) = (1, 0, -1). +// * (s, t, r) = (0, -1, -1) +// (Start from (0, 0, 0), add any multiple of these rays and we maintain +// feasibility and increase the objective.) +TEST_P(MinCostFlowTest, CertifiesInfeasibility) { + Model model; + const Variable st = model.AddContinuousVariable(0.0, 1.0); + math_opt::LinearConstraint s_balance = + model.AddLinearConstraint(st == 2.0); // flow out of s + math_opt::LinearConstraint t_balance = + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + ASSERT_OK_AND_ASSIGN( + const SolveResult result, + Solve(model, GetParam().solver_type, + {.parameters = GetParam().request_dual_rays_params})); + EXPECT_THAT(result, + TerminatesWithOneOf({TerminationReason::kInfeasible, + TerminationReason::kInfeasibleOrUnbounded})); + if (GetParam().certifies_nontrivial_infeasibility) { + // We need to check that (s, t, r) is a convex combination of (1, 0, -1) and + // (0, -1, -1). Note that solvers which do not return basic solutions (e.g. + // PDLP can and will give us a convex combination. + // + // Strategy: + // 1. check that r < -esp, t <= 0, s >= 0 + // 2. check that |s| + |t| ~= |r| + // + // NOTE: it would be better if we had a test where dual ray was unique (up + // to scaling). + ASSERT_TRUE(result.has_dual_ray()); + const DualRay& ray = result.dual_rays[0]; + ASSERT_EQ(ray.dual_values.size(), 2); + ASSERT_TRUE(ray.dual_values.contains(s_balance)); + ASSERT_TRUE(ray.dual_values.contains(t_balance)); + ASSERT_EQ(ray.reduced_costs.size(), 1); + ASSERT_TRUE(ray.reduced_costs.contains(st)); + // Check that t < 0, no obvious minimum separation... + ASSERT_LE(ray.reduced_costs.at(st), -1e-1); + // Check that s ~=> 0. + ASSERT_GE(ray.dual_values.at(s_balance), -1.0e-6); + // Check that t ~<= 0. + ASSERT_LE(ray.dual_values.at(t_balance), 1.0e-6); + const double lhs = std::abs(ray.dual_values.at(s_balance)) + + std::abs(ray.dual_values.at(t_balance)); + const double rhs = std::abs(ray.reduced_costs.at(st)); + // check that lhs ~= rhs, up to a relative error. Note that we know that rhs + // is nonzero already. + EXPECT_NEAR(lhs / rhs, 1.0, 1e-4); + } +} + +// Primal: +// min 3st +// s.t st = 2 (s) +// -st = -2 (t) +// st in [0,5] (r) +// +// Dual: +// max 2s - 2t + 5r +// s.t. s - t + r <= 3 +// r <= 0 +// +// Optimal objective value: 6.0 +// Primal solution: st = 2 +// The problem has one dual basic optimal solution: +// (s, t, r) = (0, -3, 0) +// And a ray along which all solution are optimal: +// ray = (1, 1, 0) +// I.e., for all a >= 0, we have that +// (s, t, r) = (a, a-3, 0) +// is optimal. +TEST_P(MinCostFlowTest, DualSolution) { + if (!GetParam().returns_dual_solution) { + return; + } + Model model; + const Variable st = model.AddContinuousVariable(0.0, 5.0); + math_opt::LinearConstraint s_balance = + model.AddLinearConstraint(st == 2.0); // flow out of s + math_opt::LinearConstraint t_balance = + model.AddLinearConstraint(-st == -2.0); // flow into t + model.Minimize(3.0 * st); + VariableMap expected_reduced_costs = {{st, 0.0}}; + ASSERT_OK_AND_ASSIGN(const SolveResult result, + Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters})); + ASSERT_THAT(result, IsOptimal(6.0)); + ASSERT_TRUE(result.has_dual_feasible_solution()); + EXPECT_THAT(result.reduced_costs(), IsNear(expected_reduced_costs)); + std::vector dual_keys; + for (const auto& [dual_key, value] : result.dual_values()) { + dual_keys.push_back(dual_key); + } + ASSERT_THAT(dual_keys, UnorderedElementsAre(s_balance, t_balance)); + double s_val = result.dual_values().at(s_balance); + double t_val = result.dual_values().at(t_balance); + LOG(INFO) << "s_dual: " << s_val << ", t_dual: " << t_val; + // Fix uniqueness, subtract off s_val from both + t_val -= s_val; + EXPECT_NEAR(t_val, -3.0, 1.0e-6); +} + +// A valid Min-Cost Flow problem formulated as an LP. +// +// Nodes: A (supply=20), B (supply=-5), C (supply=-15) +// Arcs: +// A->B (capacity=15, cost=2) +// B->C (capacity=10, cost=3) +// A->C (capacity=20, cost=10) +TEST_P(MinCostFlowTest, ValidMinCostFlow) { + Model model; + + // Create arcs as variables. + const Variable ab = model.AddContinuousVariable(0.0, 15.0, "ab"); + const Variable bc = model.AddContinuousVariable(0.0, 10.0, "bc"); + const Variable ac = model.AddContinuousVariable(0.0, 20.0, "ac"); + + // Create flow conservation constraints: outflow - inflow = supply + model.AddLinearConstraint(ab + ac == 20.0, "NodeA"); + model.AddLinearConstraint(bc - ab == -5.0, "NodeB"); + model.AddLinearConstraint(-bc - ac == -15.0, "NodeC"); + + // Objective: Minimize 2*ab + 3*bc + 10*ac + model.Minimize(2.0 * ab + 3.0 * bc + 10.0 * ac); + + // Optimal flow: + // A must send 20 units of flow. + // The path A->B->C costs 2+3=5 per unit, which is cheaper than A->C (cost + // 10). A can send 15 units via A->B (saturating A->B capacity). B consumes 5 + // units (supply -5 = demand 5). B forwards the remaining 10 units to C via + // B->C, saturating B->C capacity. + // A must send 5 more units. It must send them via A->C. C receives 10 units + // from B and 5 units from A, satisfying its demand of 15. + // Costs: + // ab: 15 units * 2 = 30 + // bc: 10 units * 3 = 30 + // ac: 5 units * 10 = 50 + // Total cost: 110 + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution( + 110.0, {{ab, 15.0}, {bc, 10.0}, {ac, 5.0}}, 1e-4))); +} + +// A valid Max-Cost Flow problem formulated as an LP. +// +// Nodes: A (supply=20), B (supply=-5), C (supply=-15) +// Arcs: +// A->B (capacity=15, cost=2) +// B->C (capacity=10, cost=3) +// A->C (capacity=20, cost=10) +TEST_P(MinCostFlowTest, ValidMaxCostFlow) { + Model model; + + const Variable ab = model.AddContinuousVariable(0.0, 15.0, "ab"); + const Variable bc = model.AddContinuousVariable(0.0, 10.0, "bc"); + const Variable ac = model.AddContinuousVariable(0.0, 20.0, "ac"); + + model.AddLinearConstraint(ab + ac == 20.0, "NodeA"); + model.AddLinearConstraint(bc - ab == -5.0, "NodeB"); + model.AddLinearConstraint(-bc - ac == -15.0, "NodeC"); + + // Objective: Maximize 2*ab + 3*bc + 10*ac + model.Maximize(2.0 * ab + 3.0 * bc + 10.0 * ac); + + // Optimal flow: + // A must send 20 units of flow. The path A->C has the highest cost (or + // profit) of 10 per unit, so we send 15 units via A->C, saturating C's demand + // of 15. + // A must send 5 more units. It sends them via A->B. B consumes all 5 units + // (supply -5 = demand 5). We don't send any flow via B->C. + // Costs: + // ab: 5 units * 2 = 10 + // bc: 0 units * 3 = 0 + // ac: 15 units * 10 = 150 + // Total cost: 160 + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution( + 160.0, {{ab, 5.0}, {bc, 0.0}, {ac, 15.0}}, 1e-3))); +} + +// This test is taken from +// ortools/graph/min_cost_flow_test.cc +TEST_P(MinCostFlowTest, FeasibleProblem) { + Model model; + + constexpr std::array kNodeSupply = {20.0, 10.0, 25.0, -11.0, + -13.0, -17.0, -14.0}; + constexpr std::array kSrc = {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2}; + constexpr std::array kDst = {3, 4, 5, 6, 3, 4, 5, 6, 3, 4, 5, 6}; + constexpr std::array kCost = {0.0, 0.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 1.0, 0.0, 1.0, 0.0, 0.0}; + constexpr std::array kCapacity = {100.0, 100.0, 100.0, 100.0, 100.0, 100.0, + 100.0, 100.0, 100.0, 100.0, 100.0, 100.0}; + const double kExpectedFlowCost = 0.0; + constexpr std::array kExpectedFlow = {7.0, 13.0, 0.0, 0.0, 0.0, 0.0, + 10.0, 0.0, 4.0, 0.0, 7.0, 14.0}; + + std::vector nodes; + for (int i = 0; i < kNodeSupply.size(); ++i) { + nodes.push_back(model.AddLinearConstraint(kNodeSupply[i], kNodeSupply[i])); + } + + std::vector vars; + for (int arc = 0; arc < kSrc.size(); ++arc) { + const Variable v = model.AddContinuousVariable(0.0, kCapacity[arc]); + vars.push_back(v); + model.set_coefficient(nodes[kSrc[arc]], v, 1.0); + model.set_coefficient(nodes[kDst[arc]], v, -1.0); + } + model.Minimize(InnerProduct(vars, kCost)); + + VariableMap expected_values; + for (int i = 0; i < kExpectedFlow.size(); ++i) { + expected_values[vars[i]] = kExpectedFlow[i]; + } + + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(IsOptimalWithSolution(kExpectedFlowCost, + expected_values, 1e-3))); +} + +// This test is taken from +// ortools/graph/min_cost_flow_test.cc +TEST_P(MinCostFlowTest, InfeasibleProblem) { + Model model; + + constexpr std::array kNodeSupply = {20.0, 10.0, 25.0, -11.0, + -13.0, -17.0, -14.0}; + constexpr std::array kSrc = {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2}; + constexpr std::array kDst = {3, 4, 5, 6, 3, 4, 5, 6, 3, 4, 5, 6}; + constexpr std::array kCost = {0.0, 0.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 1.0, 0.0, 1.0, 0.0, 0.0}; + + std::vector nodes; + for (int i = 0; i < kNodeSupply.size(); ++i) { + nodes.push_back(model.AddLinearConstraint(kNodeSupply[i], kNodeSupply[i])); + } + + std::vector vars; + for (int arc = 0; arc < kSrc.size(); ++arc) { + const Variable v = model.AddContinuousVariable(0.0, 1.0); + vars.push_back(v); + model.set_coefficient(nodes[kSrc[arc]], v, 1.0); + model.set_coefficient(nodes[kDst[arc]], v, -1.0); + } + model.Minimize(InnerProduct(vars, kCost)); + + EXPECT_THAT(Solve(model, GetParam().solver_type, + {.parameters = GetParam().default_parameters}), + IsOkAndHolds(TerminatesWithOneOf( + {TerminationReason::kInfeasible, + TerminationReason::kInfeasibleOrUnbounded}))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/solver_tests/min_cost_flow_tests.h b/ortools/math_opt/solver_tests/min_cost_flow_tests.h new file mode 100644 index 00000000000..b7161e3d38e --- /dev/null +++ b/ortools/math_opt/solver_tests/min_cost_flow_tests.h @@ -0,0 +1,78 @@ +// 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_MATH_OPT_SOLVER_TESTS_MIN_COST_FLOW_TESTS_H_ +#define ORTOOLS_MATH_OPT_SOLVER_TESTS_MIN_COST_FLOW_TESTS_H_ + +#include +#include +#include + +#include "absl/base/attributes.h" +#include "gtest/gtest.h" +#include "ortools/math_opt/cpp/math_opt.h" + +namespace operations_research::math_opt { + +struct MinCostFlowTestParams { + std::string name ABSL_REQUIRE_EXPLICIT_INIT; + + SolverType solver_type ABSL_REQUIRE_EXPLICIT_INIT; + + // Okay to leave empty, these parameters will be applied to every test except + // the tests where we are looking for a dual ray. + SolveParameters default_parameters; + + // When set, we get an InvalidArgumentError containing this string if the flow + // problem is an LP instead of a flow problem, otherwise, we expect to solve + // it as a generic LP. + std::optional lp_not_flow_error_substring + ABSL_REQUIRE_EXPLICIT_INIT; + + // When set, we get an InvalidArgumentError if the flow problem is a MIP + // instead of a flow problem, otherwise, we expect to solve it as a MIP. + std::optional mip_not_flow_error_substring + ABSL_REQUIRE_EXPLICIT_INIT; + + // When set, we get InvalidArgumentError containing this string if the flow + // problem has floating point costs, otherwise, we expect to solve it. + std::optional floating_point_cost_error_substring + ABSL_REQUIRE_EXPLICIT_INIT; + + // When set, we get InvalidArgumentError containing this string if the flow + // problem has floating point capacities, otherwise, we expect to solve it. + std::optional floating_point_capacity_error_substring + ABSL_REQUIRE_EXPLICIT_INIT; + + // For the case where problem is infeasible not because of imbalanced flow, + // but because the arc capacity is insufficient. If true, we expect a dual + // ray, otherwise, we simply expect an infeasible termination reason. + bool certifies_nontrivial_infeasibility ABSL_REQUIRE_EXPLICIT_INIT; + // Used only on problems where we know the problem is infeasible and we want + // to see if we can get a dual ray. Leave blank if dual rays are not + // supported. + math_opt::SolveParameters request_dual_rays_params; + + // If true, on every successful solve, we also expect a dual feasible + // solution. + bool returns_dual_solution ABSL_REQUIRE_EXPLICIT_INIT; +}; + +std::ostream& operator<<(std::ostream& out, + const MinCostFlowTestParams& params); + +class MinCostFlowTest : public testing::TestWithParam {}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_SOLVER_TESTS_MIN_COST_FLOW_TESTS_H_ diff --git a/ortools/math_opt/solver_tests/mip_tests.cc b/ortools/math_opt/solver_tests/mip_tests.cc index 30e78285fcf..c151f6ae88f 100644 --- a/ortools/math_opt/solver_tests/mip_tests.cc +++ b/ortools/math_opt/solver_tests/mip_tests.cc @@ -70,6 +70,42 @@ IncrementalMipTest::IncrementalMipTest() namespace { +TEST_P(SimpleMipTest, EmptyModel) { + Model model; + EXPECT_THAT(Solve(model, GetParam().solver_type), + IsOkAndHolds(IsOptimal(0.0))); +} + +TEST_P(SimpleMipTest, OffsetOnlyMinimization) { + Model model; + model.Minimize(4.0); + EXPECT_THAT(Solve(model, GetParam().solver_type), + IsOkAndHolds(IsOptimal(4.0))); +} + +TEST_P(SimpleMipTest, OffsetOnlyMaximization) { + Model model; + model.Maximize(4.0); + EXPECT_THAT(Solve(model, GetParam().solver_type), + IsOkAndHolds(IsOptimal(4.0))); +} + +TEST_P(SimpleMipTest, OffsetMinimization) { + Model model; + const Variable x = model.AddIntegerVariable(-1.0, 2.0, "x"); + model.Minimize(2 * x + 4); + EXPECT_THAT(Solve(model, GetParam().solver_type), + IsOkAndHolds(IsOptimal(2.0))); +} + +TEST_P(SimpleMipTest, OffsetMaximization) { + Model model; + const Variable x = model.AddIntegerVariable(-1.0, 2.0, "x"); + model.Maximize(2 * x + 4); + EXPECT_THAT(Solve(model, GetParam().solver_type), + IsOkAndHolds(IsOptimal(8.0))); +} + TEST_P(SimpleMipTest, OneVarMax) { Model model; const Variable x = model.AddVariable(0.0, 4.0, false, "x"); @@ -146,6 +182,7 @@ TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) { // Xpress rounds bounds of integer variables on input, so the bounds // specified here result in [1,0]. Xpress also checks that bounds are // not contradicting, so it rejects creation of such a variable. + // see https://github.com/google/or-tools/issues/5085 GTEST_SKIP() << "Xpress does not support contradictory bounds."; } Model model; diff --git a/ortools/math_opt/solver_tests/multi_objective_tests.cc b/ortools/math_opt/solver_tests/multi_objective_tests.cc index 1cb1916ae6d..68b324458a3 100644 --- a/ortools/math_opt/solver_tests/multi_objective_tests.cc +++ b/ortools/math_opt/solver_tests/multi_objective_tests.cc @@ -374,10 +374,6 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } - if (GetParam().solver_type == SolverType::kXpress) { - GTEST_SKIP() << "Ignoring this test because Xpress does not support per " - "objective time limits at the moment"; - } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); const Objective aux_obj = model->AddMaximizationObjective( @@ -389,6 +385,11 @@ TEST_P(SimpleMultiObjectiveTest, .objective_parameters = { {aux_obj, {.time_limit = absl::Milliseconds(1)}}}}}; const auto result = Solve(*model, GetParam().solver_type, args); + if (GetParam().solver_type == SolverType::kXpress) { + EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("per-objective time limits"))); + return; + } if (!GetParam().supports_auxiliary_objectives) { EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("multiple objectives"))); @@ -409,10 +410,6 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } - if (GetParam().solver_type == SolverType::kXpress) { - GTEST_SKIP() << "Ignoring this test because Xpress does not support per " - "objective time limits at the moment"; - } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); model->AddMaximizationObjective(0, /*priority=*/1); @@ -422,6 +419,11 @@ TEST_P(SimpleMultiObjectiveTest, .objective_parameters = {{model->primary_objective(), {.time_limit = absl::Milliseconds(1)}}}}}; const auto result = Solve(*model, GetParam().solver_type, args); + if (GetParam().solver_type == SolverType::kXpress) { + EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("per-objective time limits"))); + return; + } if (!GetParam().supports_auxiliary_objectives) { EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("multiple objectives"))); @@ -474,10 +476,6 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } - if (GetParam().solver_type == SolverType::kXpress) { - GTEST_SKIP() << "Ignoring this test because Xpress does not support per " - "objective time limits at the moment"; - } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); SolveArguments args = { @@ -486,13 +484,18 @@ TEST_P(SimpleMultiObjectiveTest, .objective_parameters = {{model->primary_objective(), {.time_limit = absl::Seconds(10)}}}}}; args.parameters.time_limit = absl::Milliseconds(1); - ASSERT_OK_AND_ASSIGN(const SolveResult result, - Solve(*model, GetParam().solver_type, args)); - EXPECT_THAT(result, TerminatesWithLimit(Limit::kTime)); + const auto result = Solve(*model, GetParam().solver_type, args); + if (GetParam().solver_type == SolverType::kXpress) { + EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("per-objective time limits"))); + return; + } + ASSERT_OK(result); + EXPECT_THAT(*result, TerminatesWithLimit(Limit::kTime)); // Solvers do not stop very precisely, use a large number to avoid flaky // tests. Do NOT try to fine tune this to be small, it is hard to get right // for all compilation modes (e.g., debug, asan). - EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1)); + EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1)); } // We test that all solvers that do not support multi-objective models error @@ -598,10 +601,6 @@ TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) { if (!GetParam().supports_auxiliary_objectives) { GTEST_SKIP() << kNoMultiObjectiveSupportMessage; } - if (!GetParam().supports_incremental_objective_add_and_delete) { - GTEST_SKIP() - << "Ignoring this test as it requires support for incremental solve"; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -655,10 +654,6 @@ TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) { if (!GetParam().supports_auxiliary_objectives) { GTEST_SKIP() << kNoMultiObjectiveSupportMessage; } - if (!GetParam().supports_incremental_objective_add_and_delete) { - GTEST_SKIP() - << "Ignoring this test as it requires support for incremental solve"; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); diff --git a/ortools/math_opt/solver_tests/second_order_cone_tests.cc b/ortools/math_opt/solver_tests/second_order_cone_tests.cc index a2ce56b2e74..68fd24a034b 100644 --- a/ortools/math_opt/solver_tests/second_order_cone_tests.cc +++ b/ortools/math_opt/solver_tests/second_order_cone_tests.cc @@ -62,9 +62,6 @@ constexpr double kTolerance = 1.0e-3; constexpr absl::string_view kNoSocSupportMessage = "This test is disabled as the solver does not support second-order cone " "constraints"; -constexpr absl::string_view kNoIncrementalAddAndDeletes = - "This test is disabled as the solver does not support incremental add and " - "deletes"; // Builds the simple (and uninteresting) SOC model: // @@ -188,9 +185,6 @@ TEST_P(SimpleSecondOrderConeTest, SolveModelWithSocAndLinearConstraints) { // The unique optimal solution is then (x*, y*) = (0.5, 0.5) with objective // value 1. TEST_P(IncrementalSecondOrderConeTest, LinearToSecondOrderConeUpdate) { - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << kNoIncrementalAddAndDeletes; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -254,9 +248,6 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesSecondOrderConeConstraint) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << kNoIncrementalAddAndDeletes; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -344,9 +335,6 @@ TEST_P(IncrementalSecondOrderConeTest, if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << kNoIncrementalAddAndDeletes; - } Model model; const Variable x = model.AddContinuousVariable(0.0, 2.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -389,9 +377,6 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableThatIsAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << kNoIncrementalAddAndDeletes; - } Model model; const Variable x = model.AddContinuousVariable(1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -434,9 +419,6 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableInAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } - if (!GetParam().supports_incremental_add_and_deletes) { - GTEST_SKIP() << kNoIncrementalAddAndDeletes; - } Model model; const Variable x = model.AddContinuousVariable(1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 2.0, "y"); diff --git a/ortools/math_opt/solver_tests/test_models.cc b/ortools/math_opt/solver_tests/test_models.cc index a133a304c3f..c403a2c7882 100644 --- a/ortools/math_opt/solver_tests/test_models.cc +++ b/ortools/math_opt/solver_tests/test_models.cc @@ -14,14 +14,87 @@ #include "ortools/math_opt/solver_tests/test_models.h" #include +#include +#include #include #include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/random/random.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "ortools/math_opt/cpp/math_opt.h" namespace operations_research::math_opt { +absl::string_view ToString(const TestModelClass model_class) { + switch (model_class) { + case TestModelClass::kIp: + return "ip"; + case TestModelClass::kLp: + return "lp"; + case TestModelClass::kMinCostFlow: + return "min_cost_flow"; + } + LOG(FATAL) << "unreachable"; +} + +std::ostream& operator<<(std::ostream& os, const TestModelClass& model_class) { + os << ToString(model_class); + return os; +} + +std::unique_ptr MinimalModelForTestModelClass( + const TestModelClass model_class) { + auto result = std::make_unique(); + const bool integer = model_class == TestModelClass::kIp; + const Variable st = result->AddVariable(0.0, 1.0, integer, "st"); + result->AddLinearConstraint(st == 1, "flow_out_s"); + if (model_class == TestModelClass::kMinCostFlow) { + result->AddLinearConstraint(-st == -1, "flow_in_t"); + } + result->Minimize(st); + return result; +} + +std::unique_ptr NontrivialModel(const TestModelClass model_class, + const int n, const int seed) { + // Builds and returns a maximum weight matching problem, in the format of a + // min cost flow problem. Variables are integer when the model_class is ip + // and continuous otherwise. Thus we satisfy all TestModelClasses. + auto model = std::make_unique(); + + std::vector left_nodes; + for (int i = 0; i < n; ++i) { + left_nodes.push_back( + model->AddLinearConstraint(1.0, 1.0, absl::StrCat("L_", i))); + } + + std::vector right_nodes; + for (int j = 0; j < n; ++j) { + // Note: we must put the problem in min cost flow format, so we use the + // unconventional sign of -1 here. + right_nodes.push_back( + model->AddLinearConstraint(-1.0, -1.0, absl::StrCat("R_", j))); + } + + std::mt19937 bit_gen(seed); + LinearExpression objective; + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + const Variable edge = + model->AddVariable(0.0, 1.0, model_class == TestModelClass::kIp, + absl::StrCat("E_", i, "_", j)); + model->set_coefficient(left_nodes[i], edge, 1.0); + model->set_coefficient(right_nodes[j], edge, -1.0); + objective += edge * absl::Uniform(bit_gen, 0, 1000); + } + } + + model->Minimize(objective); + return model; +} + std::unique_ptr SmallModel(const bool integer) { auto model = std::make_unique("small_model"); const Variable x_1 = model->AddVariable(0.0, 1.0, integer, "x_1"); diff --git a/ortools/math_opt/solver_tests/test_models.h b/ortools/math_opt/solver_tests/test_models.h index 95f04996670..22b175af8f0 100644 --- a/ortools/math_opt/solver_tests/test_models.h +++ b/ortools/math_opt/solver_tests/test_models.h @@ -16,11 +16,56 @@ #define ORTOOLS_MATH_OPT_SOLVER_TESTS_TEST_MODELS_H_ #include +#include +#include "absl/base/nullability.h" +#include "absl/strings/string_view.h" #include "ortools/math_opt/cpp/math_opt.h" namespace operations_research::math_opt { +// Common model types that we would want to run a test suite on. Each value +// imposes some restrictions on what can be in a mathopt Model, and there are +// functions to generate a Model meeting these restrictions below. +// +// Note that SupportedProblemStructures is a statement about what a solver can +// do, while this is a statement about what kind of model to generate. +enum class TestModelClass { + // Model contains only a single linear objective and linear constraints, all + // variables are continuous, all constraints are equality constraints, all + // variable lower bounds are zero, and each variable appears in exactly two + // constraints, once with coefficient 1, and once with coefficient -1. + // + // See go/mathopt-min-cost-flow for a precise statement of the rules. + kMinCostFlow, + + // Model contains only a single linear objective and only linear constraints, + // and all variables are continuous. + kLp, + + // Model contains only a single linear objective and only linear constraints, + // and all variables are integer. + kIp, +}; + +absl::string_view ToString(TestModelClass model_class); + +std::ostream& operator<<(std::ostream& os, const TestModelClass& model_class); + +constexpr double kMinimalModelForTestModelClassOptimalObjective = 1.0; + +// Returns a model within model_class with at least one named variable and named +// constraint and optimal objective of +// kMinimalModelForTestModelClassOptimalObjective. +absl_nonnull std::unique_ptr MinimalModelForTestModelClass( + TestModelClass model_class); + +// Returns a model within model_class that cannot be solve immediately, where +// solve time grows with n. Objective weights are drawn from a random number +// generator with the given seed. +absl_nonnull std::unique_ptr NontrivialModel(TestModelClass model_class, + int n, int seed = 0); + // Decision variables: // * x[i], i=1..3 // * y[i], i=1..3 @@ -34,7 +79,7 @@ namespace operations_research::math_opt { // Analysis: // * For IP, x[i] = 1, y[i] = 0 for all i is optimal, objective 9. // * For LP, x[i] = 1, y[i] = 0.5 for all i is optimal, objective is 12. -std::unique_ptr SmallModel(bool integer); +absl_nonnull std::unique_ptr SmallModel(bool integer); // Problem data: m = 3, n > 0, c = [5, 4, 3] // @@ -59,7 +104,8 @@ std::unique_ptr SmallModel(bool integer); // so the problem has a large initial gap. // * For LP, variable is at a bound, so likely some pivots will be required. // * The MIP has many symmetric solutions. -std::unique_ptr DenseIndependentSet(bool integer, int n = 10); +absl_nonnull std::unique_ptr DenseIndependentSet(bool integer, + int n = 10); // A hint with objective value of 5 for the model returned by // DenseIndependentSet(). @@ -80,7 +126,8 @@ ModelSolveParameters::SolutionHint DenseIndependentSetHint5(const Model& model); // * Setting an iteration of limit significantly smaller than n should prevent // an LP solver from finding an optimal solution. Specific state at such // termination is solver-dependent. -std::unique_ptr IndependentSetCompleteGraph(bool integer, int n = 10); +absl_nonnull std::unique_ptr IndependentSetCompleteGraph(bool integer, + int n = 10); } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solver_tests/test_models_test.cc b/ortools/math_opt/solver_tests/test_models_test.cc index 9c751ce1a2c..07b73afeb13 100644 --- a/ortools/math_opt/solver_tests/test_models_test.cc +++ b/ortools/math_opt/solver_tests/test_models_test.cc @@ -19,11 +19,63 @@ #include "ortools/base/gmock.h" #include "ortools/math_opt/cpp/matchers.h" #include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/testing/stream.h" namespace operations_research::math_opt { using ::testing::status::IsOkAndHolds; +TEST(TestModelClassTest, ToString) { + EXPECT_EQ(ToString(TestModelClass::kIp), "ip"); + EXPECT_EQ(StreamToString(TestModelClass::kIp), "ip"); + EXPECT_EQ(ToString(TestModelClass::kLp), "lp"); + EXPECT_EQ(StreamToString(TestModelClass::kLp), "lp"); + EXPECT_EQ(ToString(TestModelClass::kMinCostFlow), "min_cost_flow"); + EXPECT_EQ(StreamToString(TestModelClass::kMinCostFlow), "min_cost_flow"); +} + +TEST(MinimalModelForTestModelClassTest, LpCanSolve) { + std::unique_ptr model = + MinimalModelForTestModelClass(TestModelClass::kLp); + EXPECT_THAT( + Solve(*model, SolverType::kGlop), + IsOkAndHolds(IsOptimal(kMinimalModelForTestModelClassOptimalObjective))); +} + +TEST(MinimalModelTest, IpCanSolve) { + std::unique_ptr model = + MinimalModelForTestModelClass(TestModelClass::kIp); + EXPECT_THAT( + Solve(*model, SolverType::kCpSat), + IsOkAndHolds(IsOptimal(kMinimalModelForTestModelClassOptimalObjective))); +} + +TEST(MinimalModelTest, MinCostFlowCanSolve) { + std::unique_ptr model = + MinimalModelForTestModelClass(TestModelClass::kMinCostFlow); + // TODO: b/495435136 - use min cost flow solver here instead. + EXPECT_THAT( + Solve(*model, SolverType::kGlop), + IsOkAndHolds(IsOptimal(kMinimalModelForTestModelClassOptimalObjective))); +} + +TEST(NontrivialModelTest, LpCanSolve) { + std::unique_ptr model = NontrivialModel(TestModelClass::kLp, 5); + EXPECT_THAT(Solve(*model, SolverType::kGlop), IsOkAndHolds(IsOptimal())); +} + +TEST(NontrivialModelTest, IpCanSolve) { + std::unique_ptr model = NontrivialModel(TestModelClass::kIp, 5); + EXPECT_THAT(Solve(*model, SolverType::kCpSat), IsOkAndHolds(IsOptimal())); +} + +TEST(NontrivialModelTest, MinCostFlowCanSolve) { + std::unique_ptr model = + NontrivialModel(TestModelClass::kMinCostFlow, 5); + // TODO: b/495435136 - use min cost flow solver here instead. + EXPECT_THAT(Solve(*model, SolverType::kGlop), IsOkAndHolds(IsOptimal())); +} + TEST(SmallModelTest, Integer) { const std::unique_ptr model = SmallModel(/*integer=*/true); EXPECT_THAT(Solve(*model, SolverType::kGscip), IsOkAndHolds(IsOptimal(9.0))); diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index b880bb13101..84a1847775e 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -174,6 +174,7 @@ cc_library( deps = [ "//ortools/base:map_util", "//ortools/base:protoutil", + "//ortools/base:status_builder", "//ortools/base:status_macros", "//ortools/base:strong_vector", "//ortools/glop:lp_solver", @@ -202,6 +203,7 @@ cc_library( "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/cleanup", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/functional:overload", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/memory", @@ -278,11 +280,14 @@ cc_test( "//ortools/math_opt/solver_tests:lp_model_solve_parameters_tests", "//ortools/math_opt/solver_tests:lp_parameter_tests", "//ortools/math_opt/solver_tests:lp_tests", + "//ortools/math_opt/solver_tests:min_cost_flow_tests", "//ortools/math_opt/solver_tests:multi_objective_tests", "//ortools/math_opt/solver_tests:qc_tests", "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", + "//ortools/math_opt/testing:param_name", ], ) @@ -315,6 +320,7 @@ cc_test( "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", "//ortools/math_opt/testing:param_name", "@abseil-cpp//absl/status", ], @@ -429,11 +435,14 @@ cc_test( "//ortools/math_opt/solver_tests:lp_model_solve_parameters_tests", "//ortools/math_opt/solver_tests:lp_parameter_tests", "//ortools/math_opt/solver_tests:lp_tests", + "//ortools/math_opt/solver_tests:min_cost_flow_tests", "//ortools/math_opt/solver_tests:multi_objective_tests", "//ortools/math_opt/solver_tests:qc_tests", "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", + "//ortools/math_opt/testing:param_name", "//ortools/pdlp:solve_log_cc_proto", "@abseil-cpp//absl/status", ], @@ -494,6 +503,77 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "min_cost_flow_solver", + srcs = [ + "min_cost_flow_solver.cc", + "min_cost_flow_solver.h", + ], + deps = [ + "//ortools/base:map_util", + "//ortools/base:protoutil", + "//ortools/base:status_macros", + "//ortools/base:strong_int", + "//ortools/base:strong_vector", + "//ortools/graph:min_cost_flow", + "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_parameters_cc_proto", + "//ortools/math_opt:model_update_cc_proto", + "//ortools/math_opt:parameters_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt:solution_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:math_opt_proto_utils", + "//ortools/math_opt/core:solver_interface", + "//ortools/math_opt/validators:callback_validator", + "//ortools/util:fp_roundtrip_conv", + "//ortools/util:solve_interrupter", + "//ortools/util:status_macros", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/memory", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/time", + ], + alwayslink = 1, +) + +cc_test( + name = "min_cost_flow_solver_test", + srcs = ["min_cost_flow_solver_test.cc"], + tags = [ + "no_test_wasm", + "noci", + ], + deps = [ + ":glop_solver", + ":min_cost_flow_solver", + "//ortools/base:gmock", + "//ortools/base:gmock_main", + "//ortools/base:types", + "//ortools/graph:min_cost_flow", + "//ortools/math_opt/cpp:matchers", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solver_tests:callback_tests", + "//ortools/math_opt/solver_tests:generic_tests", + "//ortools/math_opt/solver_tests:invalid_input_tests", + "//ortools/math_opt/solver_tests:logical_constraint_tests", + "//ortools/math_opt/solver_tests:min_cost_flow_tests", + "//ortools/math_opt/solver_tests:test_models", + "//ortools/math_opt/testing:param_name", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/time", + ], +) + cc_test( name = "glpk_solver_test", srcs = ["glpk_solver_test.cc"], @@ -519,12 +599,14 @@ cc_test( "//ortools/math_opt/solver_tests:lp_model_solve_parameters_tests", "//ortools/math_opt/solver_tests:lp_parameter_tests", "//ortools/math_opt/solver_tests:lp_tests", + "//ortools/math_opt/solver_tests:min_cost_flow_tests", "//ortools/math_opt/solver_tests:mip_tests", "//ortools/math_opt/solver_tests:multi_objective_tests", "//ortools/math_opt/solver_tests:qc_tests", "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", "//ortools/math_opt/testing:param_name", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", @@ -605,7 +687,10 @@ cc_test( srcs = ["highs_solver_test.cc"], flaky = 1, # Highs fails to build with WASM in @zstr. - tags = ["no_test_wasm"], + tags = [ + "no_test_wasm", + "noci", + ], deps = [ ":highs_cc_proto", ":highs_solver", @@ -621,9 +706,11 @@ cc_test( "//ortools/math_opt/solver_tests:lp_model_solve_parameters_tests", "//ortools/math_opt/solver_tests:lp_parameter_tests", "//ortools/math_opt/solver_tests:lp_tests", + "//ortools/math_opt/solver_tests:min_cost_flow_tests", "//ortools/math_opt/solver_tests:mip_tests", "//ortools/math_opt/solver_tests:multi_objective_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", "//ortools/math_opt/testing:param_name", "@abseil-cpp//absl/status", ], @@ -733,11 +820,11 @@ cc_library( "//ortools/math_opt/core:math_opt_proto_utils", "//ortools/math_opt/core:solver_interface", "//ortools/math_opt/core:sparse_vector_view", - "//ortools/math_opt/cpp:math_opt", "//ortools/math_opt/solvers/xpress:g_xpress", "//ortools/math_opt/validators:callback_validator", "//ortools/third_party_solvers:xpress_environment", "//ortools/util:solve_interrupter", + "@abseil-cpp//absl/base:config", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/container:flat_hash_set", @@ -779,6 +866,7 @@ cc_test( "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", + "//ortools/math_opt/solver_tests:test_models", "//ortools/third_party_solvers:xpress_environment", "@abseil-cpp//absl/log", ], diff --git a/ortools/math_opt/solvers/CMakeLists.txt b/ortools/math_opt/solvers/CMakeLists.txt index 4f1343e053b..3b039bc2c50 100644 --- a/ortools/math_opt/solvers/CMakeLists.txt +++ b/ortools/math_opt/solvers/CMakeLists.txt @@ -116,6 +116,7 @@ if(USE_GLOP) "$" "$" "$" + "$" "$" "$" "$" @@ -166,6 +167,22 @@ ortools_cxx_test( absl::synchronization ) +ortools_cxx_test( + NAME + math_opt_solvers_min_cost_flow_solver_test + SOURCES + "min_cost_flow_solver_test.cc" + LINK_LIBRARIES + ortools::base_gmock + GTest::gmock_main + absl::status + "$" + "$" + "$" + "$" + "$" +) + if(USE_PDLP) ortools_cxx_test( NAME @@ -186,6 +203,7 @@ if(USE_PDLP) "$" "$" "$" + "$" "$" "$" "$" @@ -217,6 +235,7 @@ if(USE_GLPK) "$" "$" "$" + "$" "$" "$" "$" @@ -247,6 +266,7 @@ if(USE_HIGHS) "$" "$" "$" + "$" "$" "$" "$" diff --git a/ortools/math_opt/solvers/cp_sat_solver_test.cc b/ortools/math_opt/solvers/cp_sat_solver_test.cc index 623a272d3b7..5227b683a0f 100644 --- a/ortools/math_opt/solvers/cp_sat_solver_test.cc +++ b/ortools/math_opt/solvers/cp_sat_solver_test.cc @@ -34,6 +34,7 @@ #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/testing/param_name.h" namespace operations_research { @@ -147,14 +148,13 @@ INSTANTIATE_TEST_SUITE_P(CpSatIncrementalLogicalConstraintTest, IncrementalLogicalConstraintTest, Values(GetCpSatLogicalConstraintTestParameters())); -INSTANTIATE_TEST_SUITE_P( - CpSatInvalidInputTest, InvalidInputTest, - Values(InvalidInputTestParameters(SolverType::kCpSat, - /*use_integer_variables=*/true))); +INSTANTIATE_TEST_SUITE_P(CpSatInvalidInputTest, InvalidInputTest, + Values(InvalidInputTestParameters( + SolverType::kCpSat, TestModelClass::kIp))); INSTANTIATE_TEST_SUITE_P(CpSatInvalidParameterTest, InvalidParameterTest, Values(InvalidParameterTestParams( - SolverType::kCpSat, + SolverType::kCpSat, TestModelClass::kIp, {.objective_limit = 2.0, .best_bound_limit = 1.0}, {"objective_limit", "best_bound_limit"}))); @@ -209,12 +209,11 @@ INSTANTIATE_TEST_SUITE_P( Values(IpMultipleSolutionsTestParams(SolverType::kCpSat, {.presolve = Emphasis::kOff}))); -INSTANTIATE_TEST_SUITE_P( - CpSatGenericTest, GenericTest, - Values(GenericTestParameters(SolverType::kCpSat, - /*support_interrupter=*/true, - /*integer_variables=*/true, - /*expected_log=*/"status: OPTIMAL"))); +INSTANTIATE_TEST_SUITE_P(CpSatGenericTest, GenericTest, + Values(GenericTestParameters( + SolverType::kCpSat, + /*support_interrupter=*/true, TestModelClass::kIp, + /*expected_log=*/"status: OPTIMAL"))); INSTANTIATE_TEST_SUITE_P(CpSatInfeasibleSubsystemTest, InfeasibleSubsystemTest, testing::Values(InfeasibleSubsystemTestParameters( @@ -331,8 +330,7 @@ SolveParameters AllSolutions() { INSTANTIATE_TEST_SUITE_P( CpSatCallbackTest, CallbackTest, Values(CallbackTestParams( - SolverType::kCpSat, - /*integer_variables=*/true, + SolverType::kCpSat, TestModelClass::kIp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{CallbackEvent::kMipSolution, CallbackEvent::kMip}, diff --git a/ortools/math_opt/solvers/glop_solver.cc b/ortools/math_opt/solvers/glop_solver.cc index d979881f7f3..5e3434be854 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -19,11 +19,13 @@ #include #include #include +#include #include #include "absl/base/nullability.h" #include "absl/cleanup/cleanup.h" #include "absl/container/flat_hash_map.h" +#include "absl/functional/overload.h" #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/memory/memory.h" @@ -38,6 +40,7 @@ #include "absl/types/span.h" #include "ortools/base/map_util.h" #include "ortools/base/protoutil.h" +#include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" #include "ortools/base/strong_vector.h" #include "ortools/glop/lp_solver.h" @@ -86,75 +89,92 @@ absl::string_view SafeName(const LinearConstraintsProto& linear_constraints, return linear_constraints.names(index); } +LimitProto LimitProtoFromGlopInterruptionCause( + const glop::InterruptionCause cause) { + switch (cause) { + case glop::InterruptionCause::kTimeLimit: + return LIMIT_TIME; + case glop::InterruptionCause::kExternal: + return LIMIT_INTERRUPTED; + case glop::InterruptionCause::kIterationLimit: + return LIMIT_ITERATION; + case glop::InterruptionCause::kObjectiveLimit: + return LIMIT_OBJECTIVE; + } + // We don't use `default:` in above exhaustive switch to trigger a build error + // when using `-Wall -Werror`. + LOG(DFATAL) << "Unknown glop::InterruptionCause " << cause; + return LIMIT_UNDETERMINED; +} + absl::StatusOr BuildTermination( - const glop::ProblemStatus status, - const SolveInterrupter* absl_nullable const interrupter, - const bool is_maximize, const double objective_value) { - switch (status) { - case glop::ProblemStatus::OPTIMAL: - return OptimalTerminationProto(objective_value, objective_value); - case glop::ProblemStatus::PRIMAL_INFEASIBLE: - return InfeasibleTerminationProto( - is_maximize, - /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); - case glop::ProblemStatus::DUAL_UNBOUNDED: - return InfeasibleTerminationProto( - is_maximize, - /*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE); - case glop::ProblemStatus::PRIMAL_UNBOUNDED: - return UnboundedTerminationProto(is_maximize); - case glop::ProblemStatus::DUAL_INFEASIBLE: - return InfeasibleOrUnboundedTerminationProto( - is_maximize, - /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE); - case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED: - return InfeasibleOrUnboundedTerminationProto( - is_maximize, - /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); - case glop::ProblemStatus::INIT: - // Glop may flip the `interrupt_solve` atomic when it is terminated for a - // reason other than interruption so we should ignore its value. Instead - // we use the interrupter. - // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see - // comments in FillSolution). - return NoSolutionFoundTerminationProto( - is_maximize, interrupter != nullptr && interrupter->IsInterrupted() - ? LIMIT_INTERRUPTED - : LIMIT_UNDETERMINED); - case glop::ProblemStatus::DUAL_FEASIBLE: - // Glop may flip the `interrupt_solve` atomic when it is terminated for a - // reason other than interruption so we should ignore its value. Instead - // we use the interrupter. - // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see - // comments in FillSolution). - return NoSolutionFoundTerminationProto( - is_maximize, - interrupter != nullptr && interrupter->IsInterrupted() - ? LIMIT_INTERRUPTED - : LIMIT_UNDETERMINED, - objective_value); - case glop::ProblemStatus::PRIMAL_FEASIBLE: - // Glop may flip the `interrupt_solve` atomic when it is terminated for a - // reason other than interruption so we should ignore its value. Instead - // we use the interrupter. - // A primal feasible solution is only returned for PRIMAL_FEASIBLE (see - // comments in FillSolution). - return FeasibleTerminationProto( - is_maximize, - interrupter != nullptr && interrupter->IsInterrupted() - ? LIMIT_INTERRUPTED - : LIMIT_UNDETERMINED, - objective_value); - case glop::ProblemStatus::IMPRECISE: - return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); - case glop::ProblemStatus::ABNORMAL: - case glop::ProblemStatus::INVALID_PROBLEM: - return absl::InternalError( - absl::StrCat("Unexpected GLOP termination reason: ", - glop::GetProblemStatusString(status))); - } - LOG(FATAL) << "Unimplemented GLOP termination reason: " - << glop::GetProblemStatusString(status); + const glop::SolveStatus& status, const bool is_maximize, + const double objective_value) { + // Define a short name for return type of lambdas. + using Ret = absl::StatusOr; + return std::visit( + absl::Overload{ + [&](const glop::SolveStatus::Optimal&) -> Ret { + return OptimalTerminationProto(objective_value, objective_value); + }, + [&](const glop::SolveStatus::PrimalInfeasible&) -> Ret { + return InfeasibleTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); + }, + [&](const glop::SolveStatus::DualInfeasible&) -> Ret { + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE); + }, + [&](const glop::SolveStatus::InfeasibleOrUnbounded&) -> Ret { + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED); + }, + [&](const glop::SolveStatus::PrimalUnbounded&) -> Ret { + return UnboundedTerminationProto(is_maximize); + }, + [&](const glop::SolveStatus::DualUnbounded&) -> Ret { + return InfeasibleTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_FEASIBLE); + }, + [&](const glop::SolveStatus::Init& alternative) -> Ret { + // A primal feasible solution is only returned for + // SolveStatus::PrimalFeasible (see comments in FillSolution). + return NoSolutionFoundTerminationProto( + is_maximize, + LimitProtoFromGlopInterruptionCause(alternative.cause)); + }, + [&](const glop::SolveStatus::PrimalFeasible& alternative) -> Ret { + return FeasibleTerminationProto( + is_maximize, + LimitProtoFromGlopInterruptionCause(alternative.cause), + objective_value); + }, + [&](const glop::SolveStatus::DualFeasible& alternative) -> Ret { + return NoSolutionFoundTerminationProto( + is_maximize, + LimitProtoFromGlopInterruptionCause(alternative.cause), + objective_value); + }, + [&](const glop::SolveStatus::Imprecise&) -> Ret { + return TerminateForReason(is_maximize, + TERMINATION_REASON_IMPRECISE); + }, + [&](const auto& alternative) -> Ret { + // Here we check that the remaining alternatives are the expected + // ones, to be exhaustive. + using A = decltype(alternative); + static_assert( + std::is_same_v || + std::is_same_v); + return ortools::InternalErrorBuilder() + << "unexpected GLOP termination reason: " << alternative; + }, + }, + status.value); } // Returns an InvalidArgumentError if the provided parameters are invalid. @@ -606,21 +626,22 @@ InvertedBounds GlopSolver::ListInvertedBounds() const { return inverted_bounds; } -void GlopSolver::FillSolution(const glop::ProblemStatus status, +void GlopSolver::FillSolution(const glop::SolveStatus& status, const ModelSolveParametersProto& model_parameters, SolveResultProto& solve_result) { // Meaningful solutions are available if optimality is proven in // preprocessing or after 1 simplex iteration. - // TODO(b/195295177): Discuss what to do with glop::ProblemStatus::IMPRECISE + // + // TODO(b/195295177): Discuss what to do with glop::SolveStatus::Imprecise // looks like it may be set also when rays are imprecise. const bool phase_I_solution_available = - (status == glop::ProblemStatus::INIT) && + status.Is() && (lp_solver_.GetNumberOfSimplexIterations() > 0); - if (status != glop::ProblemStatus::OPTIMAL && - status != glop::ProblemStatus::PRIMAL_FEASIBLE && - status != glop::ProblemStatus::DUAL_FEASIBLE && - status != glop::ProblemStatus::PRIMAL_UNBOUNDED && - status != glop::ProblemStatus::DUAL_UNBOUNDED && + if (!status.Is() && + !status.Is() && + !status.Is() && + !status.Is() && + !status.Is() && !phase_I_solution_available) { return; } @@ -635,18 +656,18 @@ void GlopSolver::FillSolution(const glop::ProblemStatus status, // Fill in feasibility statuses // Note: if we reach here and status != OPTIMAL, then at least 1 simplex // iteration has been executed. - if (status == glop::ProblemStatus::OPTIMAL) { + if (status.Is()) { primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); basis->set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE); dual_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); - } else if (status == glop::ProblemStatus::PRIMAL_FEASIBLE) { + } else if (status.Is()) { // Solve reached phase II of primal simplex and current basis is not // optimal. Hence basis is primal feasible, but cannot be dual feasible. // Dual solution could still be feasible. primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); dual_solution->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); basis->set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE); - } else if (status == glop::ProblemStatus::DUAL_FEASIBLE) { + } else if (status.Is()) { // Solve reached phase II of dual simplex and current basis is not optimal. // Hence basis is dual feasible, but cannot be primal feasible. In addition, // glop applies dual feasibility correction in dual simplex so feasibility @@ -729,15 +750,14 @@ absl::Status GlopSolver::FillSolveStats(const absl::Duration solve_time, } absl::StatusOr GlopSolver::MakeSolveResult( - const glop::ProblemStatus status, + const glop::SolveStatus& status, const ModelSolveParametersProto& model_parameters, - const SolveInterrupter* absl_nullable const interrupter, const absl::Duration solve_time) { SolveResultProto solve_result; - OR_ASSIGN_OR_RETURN(*solve_result.mutable_termination(), - BuildTermination(status, interrupter, - linear_program_.IsMaximizationProblem(), - lp_solver_.GetObjectiveValue())); + OR_ASSIGN_OR_RETURN( + *solve_result.mutable_termination(), + BuildTermination(status, linear_program_.IsMaximizationProblem(), + lp_solver_.GetObjectiveValue())); FillSolution(status, model_parameters, solve_result); OR_RETURN_IF_ERROR( FillSolveStats(solve_time, *solve_result.mutable_solve_stats())); @@ -823,10 +843,10 @@ absl::StatusOr GlopSolver::Solve( // status. OR_RETURN_IF_ERROR(ListInvertedBounds().ToStatus()); - const glop::ProblemStatus status = - lp_solver_.SolveWithTimeLimit(linear_program_, *time_limit); + const glop::SolveStatus status = + lp_solver_.SolveWithDetails(linear_program_, *time_limit); const absl::Duration solve_time = absl::Now() - start; - return MakeSolveResult(status, model_parameters, interrupter, solve_time); + return MakeSolveResult(status, model_parameters, solve_time); } absl::StatusOr> GlopSolver::New( diff --git a/ortools/math_opt/solvers/glop_solver.h b/ortools/math_opt/solvers/glop_solver.h index 1778974ac24..cb27bc31fba 100644 --- a/ortools/math_opt/solvers/glop_solver.h +++ b/ortools/math_opt/solvers/glop_solver.h @@ -88,13 +88,12 @@ class GlopSolver : public SolverInterface { // Returns the ids of variables and linear constraints with inverted bounds. InvertedBounds ListInvertedBounds() const; - void FillSolution(glop::ProblemStatus status, + void FillSolution(const glop::SolveStatus& status, const ModelSolveParametersProto& model_parameters, SolveResultProto& solve_result); absl::StatusOr MakeSolveResult( - glop::ProblemStatus status, + const glop::SolveStatus& status, const ModelSolveParametersProto& model_parameters, - const SolveInterrupter* absl_nullable interrupter, absl::Duration solve_time); absl::Status FillSolveStats(absl::Duration solve_time, diff --git a/ortools/math_opt/solvers/glop_solver_test.cc b/ortools/math_opt/solvers/glop_solver_test.cc index 2dd5643651f..811250a56c6 100644 --- a/ortools/math_opt/solvers/glop_solver_test.cc +++ b/ortools/math_opt/solvers/glop_solver_test.cc @@ -28,11 +28,14 @@ #include "ortools/math_opt/solver_tests/lp_model_solve_parameters_tests.h" #include "ortools/math_opt/solver_tests/lp_parameter_tests.h" #include "ortools/math_opt/solver_tests/lp_tests.h" +#include "ortools/math_opt/solver_tests/min_cost_flow_tests.h" #include "ortools/math_opt/solver_tests/multi_objective_tests.h" #include "ortools/math_opt/solver_tests/qc_tests.h" #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" +#include "ortools/math_opt/testing/param_name.h" namespace operations_research { namespace math_opt { @@ -60,14 +63,18 @@ SimpleLpTestParameters ForcePrimalRays() { /*disallows_infeasible_or_unbounded=*/true); } +SolveParameters ForceDualRaysSolveParameters() { + return SolveParameters{ + .lp_algorithm = LPAlgorithm::kDualSimplex, + .presolve = Emphasis::kOff, + .scaling = Emphasis::kOff, + }; +} + SimpleLpTestParameters ForceDualRays() { - SolveParameters parameters; - parameters.presolve = Emphasis::kOff; - parameters.scaling = Emphasis::kOff; - parameters.lp_algorithm = LPAlgorithm::kDualSimplex; - parameters.enable_output = true; return SimpleLpTestParameters( - SolverType::kGlop, parameters, /*supports_duals=*/true, + SolverType::kGlop, ForceDualRaysSolveParameters(), + /*supports_duals=*/true, /*supports_basis=*/true, /*ensures_primal_ray=*/false, /*ensures_dual_ray=*/true, /*disallows_infeasible_or_unbounded=*/false); @@ -77,6 +84,21 @@ INSTANTIATE_TEST_SUITE_P(GlopSimpleLpTest, SimpleLpTest, testing::Values(GlopDefaults(), ForcePrimalRays(), ForceDualRays())); +INSTANTIATE_TEST_SUITE_P( + GlopMinCostFlowTest, MinCostFlowTest, + testing::Values(MinCostFlowTestParams{ + .name = "glop", + .solver_type = math_opt::SolverType::kGlop, + .lp_not_flow_error_substring = std::nullopt, + .mip_not_flow_error_substring = "integer variables", + .floating_point_cost_error_substring = std::nullopt, + .floating_point_capacity_error_substring = std::nullopt, + .certifies_nontrivial_infeasibility = true, + .request_dual_rays_params = ForceDualRaysSolveParameters(), + .returns_dual_solution = true, + }), + ParamName{}); + std::vector MakeStatusTestConfigs() { std::vector test_parameters; for (bool skip_presolve : {true, false}) { @@ -205,20 +227,19 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(GlopInvalidInputTest, InvalidInputTest, testing::Values(InvalidInputTestParameters( - SolverType::kGlop, - /*use_integer_variables=*/false))); + SolverType::kGlop, TestModelClass::kLp))); INSTANTIATE_TEST_SUITE_P( GlopInvalidParameterTest, InvalidParameterTest, testing::Values( - InvalidParameterTestParams(SolverType::kGlop, + InvalidParameterTestParams(SolverType::kGlop, TestModelClass::kLp, { .solution_limit = 3, .heuristics = Emphasis::kVeryHigh, }, {"solution_limit", "heuristics"}), InvalidParameterTestParams( - SolverType::kGlop, + SolverType::kGlop, TestModelClass::kLp, { .glop = []() { @@ -258,7 +279,7 @@ INSTANTIATE_TEST_SUITE_P(GlopLpBasisStartTest, LpBasisStartTest, INSTANTIATE_TEST_SUITE_P(GlopGenericTest, GenericTest, testing::Values(GenericTestParameters( SolverType::kGlop, /*support_interrupter=*/true, - /*integer_variables=*/false, + TestModelClass::kLp, /*expected_log=*/"status: OPTIMAL"))); INSTANTIATE_TEST_SUITE_P(GlopMessageCallbackTest, MessageCallbackTest, @@ -270,8 +291,7 @@ INSTANTIATE_TEST_SUITE_P(GlopMessageCallbackTest, MessageCallbackTest, INSTANTIATE_TEST_SUITE_P( GlopCallbackTest, CallbackTest, - testing::Values(CallbackTestParams(SolverType::kGlop, - /*integer_variables=*/false, + testing::Values(CallbackTestParams(SolverType::kGlop, TestModelClass::kLp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, diff --git a/ortools/math_opt/solvers/glpk/BUILD.bazel b/ortools/math_opt/solvers/glpk/BUILD.bazel index d89a85bf6db..7c73efdeee0 100644 --- a/ortools/math_opt/solvers/glpk/BUILD.bazel +++ b/ortools/math_opt/solvers/glpk/BUILD.bazel @@ -14,7 +14,6 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") -# Code specific to GLPK used in glpk_solver.cc. package(default_visibility = ["//ortools/math_opt/solvers:__subpackages__"]) cc_library( diff --git a/ortools/math_opt/solvers/glpk_solver_test.cc b/ortools/math_opt/solvers/glpk_solver_test.cc index e42ab79305f..409abc6484b 100644 --- a/ortools/math_opt/solvers/glpk_solver_test.cc +++ b/ortools/math_opt/solvers/glpk_solver_test.cc @@ -36,12 +36,14 @@ #include "ortools/math_opt/solver_tests/logical_constraint_tests.h" #include "ortools/math_opt/solver_tests/lp_model_solve_parameters_tests.h" #include "ortools/math_opt/solver_tests/lp_tests.h" +#include "ortools/math_opt/solver_tests/min_cost_flow_tests.h" #include "ortools/math_opt/solver_tests/mip_tests.h" #include "ortools/math_opt/solver_tests/multi_objective_tests.h" #include "ortools/math_opt/solver_tests/qc_tests.h" #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/testing/param_name.h" namespace operations_research { @@ -88,14 +90,13 @@ INSTANTIATE_TEST_SUITE_P(GlpkStatusTest, StatusTest, InvalidParameterTestParams InvalidThreadsParameters() { SolveParameters params; params.threads = 2; - return InvalidParameterTestParams(SolverType::kGlpk, std::move(params), - {"threads"}); + return InvalidParameterTestParams(SolverType::kGlpk, TestModelClass::kLp, + std::move(params), {"threads"}); } INSTANTIATE_TEST_SUITE_P( GlpkInvalidInputTest, InvalidInputTest, - Values(InvalidInputTestParameters(SolverType::kGlpk, - /*use_integer_variables=*/false))); + Values(InvalidInputTestParameters(SolverType::kGlpk, TestModelClass::kLp))); INSTANTIATE_TEST_SUITE_P(GlpkInvalidParameterTest, InvalidParameterTest, Values(InvalidThreadsParameters())); @@ -165,6 +166,20 @@ INSTANTIATE_TEST_SUITE_P(GlpkLpModelSolveParametersTest, GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LpParameterTest); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LpIncompleteSolveTest); +INSTANTIATE_TEST_SUITE_P( + GlpkMinCostFlowTest, MinCostFlowTest, + testing::Values(MinCostFlowTestParams{ + .name = "glpk", + .solver_type = math_opt::SolverType::kGlpk, + .lp_not_flow_error_substring = std::nullopt, + .mip_not_flow_error_substring = std::nullopt, + .floating_point_cost_error_substring = std::nullopt, + .floating_point_capacity_error_substring = std::nullopt, + .certifies_nontrivial_infeasibility = false, + .returns_dual_solution = true, + }), + ParamName{}); + std::vector GetGlpkSimpleLpTestParameters() { std::vector test_parameters; for (const auto algorithm : @@ -307,18 +322,18 @@ INSTANTIATE_TEST_SUITE_P( GlpkGenericTest, GenericTest, Values(GenericTestParameters(SolverType::kGlpk, /*support_interrupter=*/true, - /*integer_variables=*/true, + TestModelClass::kIp, /*expected_log=*/"OPTIMAL SOLUTION FOUND"), // When GLPK solves linear programs, it does not support // interruption. GenericTestParameters(SolverType::kGlpk, /*support_interrupter=*/false, - /*integer_variables=*/false, - /*expected_log=*/"OPTIMAL SOLUTION FOUND"), + TestModelClass::kLp, + /*expected_log=*/"OPTIMAL LP SOLUTION FOUND"), // GLPK has different code path for interior point. GenericTestParameters(SolverType::kGlpk, /*support_interrupter=*/false, - /*integer_variables=*/false, + TestModelClass::kLp, /*expected_log=*/"OPTIMAL SOLUTION FOUND", UseInteriorPointParameters()))); @@ -342,8 +357,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( GlpkCallbackTest, CallbackTest, - Values(CallbackTestParams(SolverType::kGlpk, - /*integer_variables=*/false, + Values(CallbackTestParams(SolverType::kGlpk, TestModelClass::kLp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, diff --git a/ortools/math_opt/solvers/gscip/BUILD.bazel b/ortools/math_opt/solvers/gscip/BUILD.bazel index 7ed0da8918a..d65a5d9e171 100644 --- a/ortools/math_opt/solvers/gscip/BUILD.bazel +++ b/ortools/math_opt/solvers/gscip/BUILD.bazel @@ -54,6 +54,7 @@ cc_library( ":gscip_cc_proto", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/time", ], ) @@ -144,6 +145,7 @@ cc_library( deps = [ ":gscip", "//ortools/base:status_macros", + "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/strings", @@ -164,6 +166,7 @@ cc_test( "//ortools/base:gmock", "//ortools/base:gmock_main", "//ortools/base:map_util", + "@abseil-cpp//absl/container:flat_hash_map", "@scip", ], ) @@ -236,6 +239,7 @@ cc_library( deps = [ ":gscip", ":gscip_callback_result", + "//ortools/base:status_builder", "//ortools/base:status_macros", "//ortools/linear_solver:scip_helper_macros", "@abseil-cpp//absl/log", @@ -276,6 +280,7 @@ cc_library( ":gscip_callback_result", ":gscip_constraint_handler", "//ortools/base:protoutil", + "//ortools/base:status_builder", "//ortools/base:status_macros", "//ortools/math_opt:callback_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", diff --git a/ortools/math_opt/solvers/gscip/gscip_constraint_handler.cc b/ortools/math_opt/solvers/gscip/gscip_constraint_handler.cc index 932febb007d..0d546eaf90c 100644 --- a/ortools/math_opt/solvers/gscip/gscip_constraint_handler.cc +++ b/ortools/math_opt/solvers/gscip/gscip_constraint_handler.cc @@ -22,6 +22,7 @@ #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/types/span.h" +#include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" #include "ortools/linear_solver/scip_helper_macros.h" #include "ortools/math_opt/solvers/gscip/gscip.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_constraint_handler_test.cc b/ortools/math_opt/solvers/gscip/gscip_constraint_handler_test.cc index e18ef78bf38..0848304217e 100644 --- a/ortools/math_opt/solvers/gscip/gscip_constraint_handler_test.cc +++ b/ortools/math_opt/solvers/gscip/gscip_constraint_handler_test.cc @@ -24,7 +24,6 @@ #include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/status_macros.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_ext.cc b/ortools/math_opt/solvers/gscip/gscip_ext.cc index 31bae5e1f00..87d7c6d8954 100644 --- a/ortools/math_opt/solvers/gscip/gscip_ext.cc +++ b/ortools/math_opt/solvers/gscip/gscip_ext.cc @@ -19,9 +19,12 @@ #include #include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/base/status_macros.h" +#include "ortools/math_opt/solvers/gscip/gscip.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/gscip/gscip_ext.h b/ortools/math_opt/solvers/gscip/gscip_ext.h index b2e51c589a2..bb3a562f9b3 100644 --- a/ortools/math_opt/solvers/gscip/gscip_ext.h +++ b/ortools/math_opt/solvers/gscip/gscip_ext.h @@ -29,6 +29,7 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_ext_test.cc b/ortools/math_opt/solvers/gscip/gscip_ext_test.cc index ea91509f266..cab703f2062 100644 --- a/ortools/math_opt/solvers/gscip/gscip_ext_test.cc +++ b/ortools/math_opt/solvers/gscip/gscip_ext_test.cc @@ -19,12 +19,13 @@ #include #include +#include +#include "absl/container/flat_hash_map.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/map_util.h" #include "ortools/math_opt/solvers/gscip/gscip.h" -#include "ortools/math_opt/solvers/gscip/gscip.pb.h" #include "ortools/math_opt/solvers/gscip/gscip_testing.h" #include "scip/scip.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.cc b/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.cc index 1c49cdc8693..8b20c16435b 100644 --- a/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.cc +++ b/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.cc @@ -25,6 +25,7 @@ #include "absl/types/span.h" #include "ortools/base/status_macros.h" #include "ortools/base/stl_util.h" +#include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/math_opt/solvers/gscip/gscip.h" #include "ortools/math_opt/solvers/gscip/gscip_ext.h" #include "scip/type_var.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.h b/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.h index 1afe6212cfc..4e8a19437d0 100644 --- a/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.h +++ b/ortools/math_opt/solvers/gscip/gscip_from_mp_model_proto.h @@ -25,8 +25,10 @@ #include #include +#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/math_opt/solvers/gscip/gscip.h" #include "ortools/math_opt/solvers/gscip/gscip_ext.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_io_test.cc b/ortools/math_opt/solvers/gscip/gscip_io_test.cc index 2d04108c4b2..2d4125f9f1f 100644 --- a/ortools/math_opt/solvers/gscip/gscip_io_test.cc +++ b/ortools/math_opt/solvers/gscip/gscip_io_test.cc @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include "gtest/gtest.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_parameters.cc b/ortools/math_opt/solvers/gscip/gscip_parameters.cc index 08e3f433c15..c85024abadd 100644 --- a/ortools/math_opt/solvers/gscip/gscip_parameters.cc +++ b/ortools/math_opt/solvers/gscip/gscip_parameters.cc @@ -19,6 +19,9 @@ #include "absl/log/check.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "ortools/math_opt/solvers/gscip/gscip.pb.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/gscip/gscip_test.cc b/ortools/math_opt/solvers/gscip/gscip_test.cc index e9ec7af393e..cbda295432b 100644 --- a/ortools/math_opt/solvers/gscip/gscip_test.cc +++ b/ortools/math_opt/solvers/gscip/gscip_test.cc @@ -33,6 +33,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" diff --git a/ortools/math_opt/solvers/gscip/gscip_testing.cc b/ortools/math_opt/solvers/gscip/gscip_testing.cc index c9a26154fce..07cd2f15420 100644 --- a/ortools/math_opt/solvers/gscip/gscip_testing.cc +++ b/ortools/math_opt/solvers/gscip/gscip_testing.cc @@ -21,6 +21,10 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/solvers/gscip/gscip.h" +#include "ortools/math_opt/solvers/gscip/gscip.pb.h" #include "ortools/math_opt/solvers/gscip/gscip_parameters.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/gscip/math_opt_gscip_solver_constraint_handler.cc b/ortools/math_opt/solvers/gscip/math_opt_gscip_solver_constraint_handler.cc index 8542fce6c0a..8f1cf4e0b2c 100644 --- a/ortools/math_opt/solvers/gscip/math_opt_gscip_solver_constraint_handler.cc +++ b/ortools/math_opt/solvers/gscip/math_opt_gscip_solver_constraint_handler.cc @@ -21,6 +21,7 @@ #include "absl/time/clock.h" #include "absl/time/time.h" #include "ortools/base/protoutil.h" +#include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" diff --git a/ortools/math_opt/solvers/gscip_solver_test.cc b/ortools/math_opt/solvers/gscip_solver_test.cc index 541385e1f9f..f70aff8cc66 100644 --- a/ortools/math_opt/solvers/gscip_solver_test.cc +++ b/ortools/math_opt/solvers/gscip_solver_test.cc @@ -38,6 +38,7 @@ #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/solvers/gscip/gscip_parameters.h" #include "ortools/math_opt/testing/param_name.h" #include "ortools/port/scoped_std_stream_capture.h" @@ -177,10 +178,9 @@ INSTANTIATE_TEST_SUITE_P(GscipIncrementalLogicalConstraintTest, IncrementalLogicalConstraintTest, Values(GetGscipLogicalConstraintTestParameters())); -INSTANTIATE_TEST_SUITE_P( - GScipInvalidInputTest, InvalidInputTest, - Values(InvalidInputTestParameters(SolverType::kGscip, - /*use_integer_variables=*/true))); +INSTANTIATE_TEST_SUITE_P(GScipInvalidInputTest, InvalidInputTest, + Values(InvalidInputTestParameters( + SolverType::kGscip, TestModelClass::kIp))); SolveParameters StopBeforeOptimal() { return {.node_limit = 1, @@ -240,7 +240,8 @@ InvalidParameterTestParams MakeGScipBadParams() { // TODO(b/168069105): for solver specific errors, we should collect all // errors, not just the first. Then set int_param "parallel/maxnthreads" to // -4 (an invalid value). - return InvalidParameterTestParams(SolverType::kGscip, std::move(parameters), + return InvalidParameterTestParams(SolverType::kGscip, TestModelClass::kIp, + std::move(parameters), {"SCIP error code -12"}); } @@ -273,8 +274,7 @@ SolveParameters ReachEventNode() { INSTANTIATE_TEST_SUITE_P( GscipCallbackTest, CallbackTest, - Values(CallbackTestParams(SolverType::kGscip, - /*integer_variables=*/true, + Values(CallbackTestParams(SolverType::kGscip, TestModelClass::kIp, /*add_lazy_constraints=*/true, /*add_cuts=*/true, /*supported_events=*/ @@ -322,11 +322,11 @@ INSTANTIATE_TEST_SUITE_P( GscipGenericTest, GenericTest, Values(GenericTestParameters(SolverType::kGscip, /*support_interrupter=*/true, - /*integer_variables=*/false, + TestModelClass::kLp, /*expected_log=*/"[optimal solution found]"), GenericTestParameters(SolverType::kGscip, /*support_interrupter=*/true, - /*integer_variables=*/true, + TestModelClass::kIp, /*expected_log=*/"[optimal solution found]"))); INSTANTIATE_TEST_SUITE_P(GscipInfeasibleSubsystemTest, InfeasibleSubsystemTest, diff --git a/ortools/math_opt/solvers/gurobi/BUILD.bazel b/ortools/math_opt/solvers/gurobi/BUILD.bazel index de1a72d1480..e16fd67ce2c 100644 --- a/ortools/math_opt/solvers/gurobi/BUILD.bazel +++ b/ortools/math_opt/solvers/gurobi/BUILD.bazel @@ -13,6 +13,11 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") +package(default_visibility = [ + "//ortools/gurobi:__subpackages__", + "//ortools/math_opt:__subpackages__", +]) + cc_library( name = "g_gurobi", srcs = [ diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index 25c3f4c0aeb..198b08511e3 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -142,14 +142,6 @@ class ScopedCallback { UserCallbackData user_cb_data_; }; -// Returns true if both keys are equal. -bool AreISVKeyEqual(const GurobiIsvKey& key, - const GurobiInitializerProto::ISVKey& proto_key) { - return key.name == proto_key.name() && - key.application_name == proto_key.application_name() && - key.expiration == proto_key.expiration() && key.key == proto_key.key(); -} - } // namespace void GurobiFreeEnv::operator()(GRBenv* const env) const { diff --git a/ortools/math_opt/solvers/highs_solver_test.cc b/ortools/math_opt/solvers/highs_solver_test.cc index 388b48a6680..8e77ded496a 100644 --- a/ortools/math_opt/solvers/highs_solver_test.cc +++ b/ortools/math_opt/solvers/highs_solver_test.cc @@ -42,9 +42,11 @@ #include "ortools/math_opt/solver_tests/lp_model_solve_parameters_tests.h" #include "ortools/math_opt/solver_tests/lp_parameter_tests.h" #include "ortools/math_opt/solver_tests/lp_tests.h" +#include "ortools/math_opt/solver_tests/min_cost_flow_tests.h" #include "ortools/math_opt/solver_tests/mip_tests.h" #include "ortools/math_opt/solver_tests/multi_objective_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/solvers/highs.pb.h" #include "ortools/math_opt/testing/param_name.h" @@ -85,11 +87,11 @@ INSTANTIATE_TEST_SUITE_P( HighsGenericTest, GenericTest, Values(GenericTestParameters(SolverType::kHighs, /*support_interrupter=*/false, - /*integer_variables=*/false, + TestModelClass::kLp, /*expected_log=*/"HiGHS run time"), GenericTestParameters(SolverType::kHighs, /*support_interrupter=*/false, - /*integer_variables=*/true, + TestModelClass::kIp, /*expected_log=*/"Solving report"))); // These tests require callback support. @@ -108,6 +110,20 @@ INSTANTIATE_TEST_SUITE_P( /*supports_best_bound_limit=*/true, /*reports_limits=*/true))); +INSTANTIATE_TEST_SUITE_P( + HighsMinCostFlowTest, MinCostFlowTest, + testing::Values(MinCostFlowTestParams{ + .name = "highs", + .solver_type = math_opt::SolverType::kHighs, + .lp_not_flow_error_substring = std::nullopt, + .mip_not_flow_error_substring = std::nullopt, + .floating_point_cost_error_substring = std::nullopt, + .floating_point_capacity_error_substring = std::nullopt, + .certifies_nontrivial_infeasibility = false, + .returns_dual_solution = true, + }), + ParamName{}); + ParameterSupport HighsMipParameterSupport() { return {.supports_node_limit = true, .supports_solution_limit_one = true, @@ -225,17 +241,16 @@ INSTANTIATE_TEST_SUITE_P(HighsStatusTest, StatusTest, INSTANTIATE_TEST_SUITE_P( HighsMessageCallbackTest, MessageCallbackTest, - Values( - MessageCallbackTestParams(SolverType::kHighs, - /*support_message_callback=*/true, - /*support_interrupter=*/false, - /*integer_variables=*/false, - /*ending_substring=*/"HiGHS run time"), - MessageCallbackTestParams(SolverType::kHighs, - /*support_message_callback=*/true, - /*support_interrupter=*/false, - /*integer_variables=*/true, - /*ending_substring=*/"LP iterations 0"))); + Values(MessageCallbackTestParams(SolverType::kHighs, + /*support_message_callback=*/true, + /*support_interrupter=*/false, + /*integer_variables=*/false, + /*ending_substring=*/"HiGHS run time"), + MessageCallbackTestParams(SolverType::kHighs, + /*support_message_callback=*/true, + /*support_interrupter=*/false, + /*integer_variables=*/true, + /*ending_substring=*/"(heuristics)"))); // HiGHS does not support callbacks other than message callback. GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CallbackTest); diff --git a/ortools/math_opt/solvers/min_cost_flow_solver.cc b/ortools/math_opt/solvers/min_cost_flow_solver.cc new file mode 100644 index 00000000000..fd8b959d638 --- /dev/null +++ b/ortools/math_opt/solvers/min_cost_flow_solver.cc @@ -0,0 +1,484 @@ +// 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/math_opt/solvers/min_cost_flow_solver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "ortools/base/map_util.h" +#include "ortools/base/protoutil.h" +#include "ortools/base/status_macros.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" +#include "ortools/graph/min_cost_flow.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/validators/callback_validator.h" +#include "ortools/util/fp_roundtrip_conv.h" +#include "ortools/util/solve_interrupter.h" +#include "ortools/util/status_macros.h" + +namespace operations_research::math_opt { + +namespace { + +using CostValue = SimpleMinCostFlow::CostValue; +using FlowQuantity = SimpleMinCostFlow::FlowQuantity; +using LinearConstraintId = int64_t; +DEFINE_STRONG_INT_TYPE(NodeIndex, int32_t); + +struct MinCostFlowArc { + std::optional src = std::nullopt; + std::optional dst = std::nullopt; +}; + +// MinCostFlow solver does not support any problem structures. +constexpr SupportedProblemStructures kMinCostFlowSupportedStructures; + +// This tolerance is used to check if a value is close enough to an integer to +// be considered an integer. +// This is similar to solver-specific tolerances such as `IntFeasTol` for +// Gurobi. The value 1e-6 is derived from the default values of such tolerances +// for other solvers to have consistent behavior. +// Ideally, we should have an integrality tolerance exposed in the +// SolveParametersProto that is propagated to all solvers that need it. +constexpr double kMinCostFlowIntegralityTolerance = 1e-6; + +constexpr absl::string_view kModelNotSupportedPrefix = + "model structure is not supported by MinCostFlow solver: "; + +// Rounds a double value to an integer of type IntType, checking that it is +// close enough to an integer and that it is within the range of representable +// numbers for IntType. +template +absl::StatusOr RoundToInteger(const double value, + const absl::string_view type_name) { + // We use `! <=` here instead of `>` so that we reject NaN as well. + if (!(std::abs(value - std::round(value)) <= + kMinCostFlowIntegralityTolerance)) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "input value " + << RoundTripDoubleFormat(value) + << " is not close enough to an integer"; + } + const double rounded = std::round(value); + if (rounded <= -static_cast(std::numeric_limits::max())) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "input value " + << RoundTripDoubleFormat(value) + << " is too small to be represented as a " << type_name; + } + if (rounded >= static_cast(std::numeric_limits::max())) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "input value " + << RoundTripDoubleFormat(value) + << " is too large to be represented as a " << type_name; + } + return static_cast(rounded); +} + +absl::Status CheckSolveParameters(const SolveParametersProto& parameters) { + std::vector warnings; + + if (parameters.has_time_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'time_limit' parameter"); + } + if (parameters.has_iteration_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'iteration_limit' parameter"); + } + if (parameters.has_node_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'node_limit' parameter"); + } + if (parameters.has_cutoff_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'cutoff_limit' parameter"); + } + if (parameters.has_objective_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'objective_limit' parameter"); + } + if (parameters.has_best_bound_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'best_bound_limit' parameter"); + } + if (parameters.has_solution_limit()) { + warnings.push_back( + "MinCostFlow solver does not support 'solution_limit' parameter"); + } + if (parameters.has_threads()) { + warnings.push_back( + "MinCostFlow solver does not support 'threads' parameter"); + } + if (parameters.has_random_seed()) { + warnings.push_back( + "MinCostFlow solver does not support 'random_seed' parameter"); + } + if (parameters.has_absolute_gap_tolerance()) { + warnings.push_back( + "MinCostFlow solver does not support 'absolute_gap_tolerance' " + "parameter"); + } + if (parameters.has_relative_gap_tolerance()) { + warnings.push_back( + "MinCostFlow solver does not support 'relative_gap_tolerance' " + "parameter"); + } + if (parameters.has_solution_pool_size()) { + warnings.push_back( + "MinCostFlow solver does not support 'solution_pool_size' parameter"); + } + if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + warnings.push_back( + "MinCostFlow solver does not support 'lp_algorithm' parameter"); + } + if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { + warnings.push_back( + "MinCostFlow solver does not support 'presolve' parameter"); + } + if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("MinCostFlow solver does not support 'cuts' parameter"); + } + if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + warnings.push_back( + "MinCostFlow solver does not support 'heuristics' parameter"); + } + if (parameters.scaling() != EMPHASIS_UNSPECIFIED) { + warnings.push_back( + "MinCostFlow solver does not support 'scaling' parameter"); + } + + if (!warnings.empty()) { + return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr> MinCostFlowSolver::New( + const ModelProto& model, const InitArgs& /*init_args*/) { + OR_RETURN_IF_ERROR( + ModelIsSupported(model, kMinCostFlowSupportedStructures, "MinCostFlow")); + + const bool is_maximize = model.objective().maximize(); + const double objective_offset = model.objective().offset(); + + const LinearConstraintsProto& constraints = model.linear_constraints(); + const VariablesProto& variables = model.variables(); + + auto mcf = std::make_unique( + /*reserve_num_nodes=*/NumConstraints(constraints), + /*reserve_num_arcs=*/NumVariables(variables)); + + // Process constraints: Validate bounds, map IDs, and set supplies. + absl::flat_hash_map constraint_to_node; + constraint_to_node.reserve(NumConstraints(constraints)); + for (int i = 0; i < NumConstraints(constraints); ++i) { + const LinearConstraintId c(constraints.ids(i)); + const double lb = constraints.lower_bounds(i); + const double ub = constraints.upper_bounds(i); + + OR_ASSIGN_OR_RETURN3(const FlowQuantity integer_lb, + RoundToInteger(lb, "flow quantity"), + _ << "invalid constraint " << c << " lower-bound"); + OR_ASSIGN_OR_RETURN3(const FlowQuantity integer_ub, + RoundToInteger(ub, "flow quantity"), + _ << "invalid constraint " << c << " upper-bound"); + if (integer_lb != integer_ub) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "constraint " << c + << " is not an equality, the lower-bound " + << RoundTripDoubleFormat(lb) << " rounds to " << integer_lb + << " but the upper-bound " << RoundTripDoubleFormat(ub) + << " rounds to " << integer_ub; + } + constraint_to_node[c] = NodeIndex(i); + + // Per convention, we interpret the bound as supply. + mcf->SetNodeSupply(i, integer_lb); + } + + // Process matrix: Aggregate src and dst nodes for each variable. + absl::flat_hash_map var_to_arc; + var_to_arc.reserve(NumVariables(variables)); + util_intops::StrongVector node_degree( + NodeIndex(NumConstraints(constraints)), 0); + + const SparseDoubleMatrixProto& matrix = model.linear_constraint_matrix(); + for (int i = 0; i < NumMatrixNonzeros(matrix); ++i) { + const LinearConstraintId c(matrix.row_ids(i)); + const VariableId v(matrix.column_ids(i)); + const double val = matrix.coefficients(i); + + auto& arc = var_to_arc[v]; + const NodeIndex node_id = constraint_to_node[c]; + + OR_ASSIGN_OR_RETURN3(const FlowQuantity integer_coefficient, + RoundToInteger(val, "flow quantity"), + _ << "invalid coefficient for constraint " << c + << " and variable " << v); + + // Per convention, we interpret +1 as outflow and -1 as inflow, consistent + // with the interpretation of the constraint bound as supply. + if (integer_coefficient == 1) { + if (arc.src.has_value()) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " has multiple +1 coefficients"; + } + arc.src = node_id; + ++node_degree[node_id]; + } else if (integer_coefficient == -1) { + if (arc.dst.has_value()) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " has multiple -1 coefficients"; + } + arc.dst = node_id; + ++node_degree[node_id]; + } else { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "matrix coefficient for variable " + << v << " in constraint " << c << " is " << integer_coefficient + << ", not +1 or -1"; + } + } + + int64_t max_node_degree = 1; + for (const NodeIndex index : node_degree.index_range()) { + max_node_degree = std::max(max_node_degree, node_degree[index]); + } + + // Process objective: Validate and map costs. + absl::flat_hash_map var_to_obj; + const SparseDoubleVectorProto& obj_coeffs = + model.objective().linear_coefficients(); + var_to_obj.reserve(obj_coeffs.ids_size()); + + for (int i = 0; i < obj_coeffs.ids_size(); ++i) { + const VariableId v(obj_coeffs.ids(i)); + const double obj_coeff = obj_coeffs.values(i); + + OR_ASSIGN_OR_RETURN3( + const CostValue integer_cost, + RoundToInteger(obj_coeff, "cost value"), + _ << "invalid objective coefficient for variable " << v); + var_to_obj[v] = integer_cost; + } + + // Process variables: Validate bounds, extract arc data, and build graph. + util_intops::StrongVector arc_to_var; + arc_to_var.reserve(ArcIndex(NumVariables(variables))); + + for (int i = 0; i < NumVariables(variables); ++i) { + const VariableId v(variables.ids(i)); + + const double lb = variables.lower_bounds(i); + const double ub = variables.upper_bounds(i); + OR_ASSIGN_OR_RETURN3(const FlowQuantity integer_lb, + RoundToInteger(lb, "flow quantity"), + _ << "invalid lower bound for variable " << v); + if (integer_lb != 0) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " lower bound is not 0"; + } + if (std::isfinite(ub)) { + OR_RETURN_IF_ERROR( + RoundToInteger(ub, "flow quantity").status()) + << "invalid upper bound for variable " << v; + } + + const MinCostFlowArc* arc_nodes = gtl::FindOrNull(var_to_arc, v); + if (arc_nodes == nullptr) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " does not appear in any constraints"; + } + + if (!arc_nodes->src.has_value()) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " does not have a +1 coefficient"; + } + const NodeIndex src = *arc_nodes->src; + + if (!arc_nodes->dst.has_value()) { + return ortools::InvalidArgumentErrorBuilder() + << kModelNotSupportedPrefix << "variable " << v + << " does not have a -1 coefficient"; + } + const NodeIndex dst = *arc_nodes->dst; + + CostValue cost = gtl::FindWithDefault(var_to_obj, v, CostValue{0}); + if (is_maximize) { + cost = -cost; + } + + // Default unbounded capacity. See BAD_CAPACITY_RANGE comment in + // min_cost_flow.h for why we don't use + // std::numeric_limits::max(). Note that this scaling does not + // prevent all possible BAD_CAPACITY_RANGE errors. + // Ideally, the min-cost flow solver should support unbounded capacities and + // scale internally. + const FlowQuantity unbounded_capacity = + std::numeric_limits::max() / max_node_degree; + + FlowQuantity capacity = unbounded_capacity; + if (ub < static_cast(unbounded_capacity)) { + capacity = static_cast(std::round(ub)); + } + + mcf->AddArcWithCapacityAndUnitCost(src.value(), dst.value(), capacity, + cost); + arc_to_var.push_back(v); + } + + return absl::WrapUnique(new MinCostFlowSolver( + std::move(mcf), std::move(arc_to_var), is_maximize, objective_offset)); +} + +MinCostFlowSolver::MinCostFlowSolver( + std::unique_ptr mcf, + util_intops::StrongVector arc_to_var, + bool is_maximize, double objective_offset) + : mcf_(std::move(mcf)), + arc_to_var_(std::move(arc_to_var)), + is_maximize_(is_maximize), + objective_offset_(objective_offset) {} + +absl::StatusOr MinCostFlowSolver::MakeSolveResult( + SimpleMinCostFlow::Status status) const { + SolveResultProto result; + TerminationProto& termination = *result.mutable_termination(); + + switch (status) { + case SimpleMinCostFlow::OPTIMAL: { + double obj_val = static_cast(mcf_->OptimalCost()); + if (is_maximize_) { + obj_val = -obj_val; + } + obj_val += objective_offset_; + + termination = OptimalTerminationProto(obj_val, obj_val); + + SolutionProto& solution = *result.add_solutions(); + PrimalSolutionProto& primal = *solution.mutable_primal_solution(); + primal.set_objective_value(obj_val); + primal.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + + SparseDoubleVectorProto& values = *primal.mutable_variable_values(); + for (const ArcIndex arc : arc_to_var_.index_range()) { + values.add_ids(arc_to_var_[arc]); + values.add_values(static_cast(mcf_->Flow(arc.value()))); + } + return result; + } + case SimpleMinCostFlow::INFEASIBLE: { + termination = InfeasibleTerminationProto( + is_maximize_, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + "problem is infeasible"); + return result; + } + case SimpleMinCostFlow::UNBALANCED: { + termination = InfeasibleTerminationProto( + is_maximize_, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + "problem is unbalanced (sum of supplies != sum of demands)"); + return result; + } + case SimpleMinCostFlow::FEASIBLE: { + return ortools::InternalErrorBuilder() + << "MinCostFlow solver returned status " << StatusName(status) + << ", which is not expected for this solver"; + } + case SimpleMinCostFlow::NOT_SOLVED: + case SimpleMinCostFlow::BAD_RESULT: + case SimpleMinCostFlow::BAD_COST_RANGE: + case SimpleMinCostFlow::BAD_CAPACITY_RANGE: { + termination = TerminateForReason( + is_maximize_, TERMINATION_REASON_OTHER_ERROR, + absl::StrCat("MinCostFlow solver failed: ", StatusName(status))); + return result; + } + } + + return ortools::InternalErrorBuilder() << "unrecognized status: " << status; +} + +absl::StatusOr MinCostFlowSolver::Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& /*model_parameters*/, + MessageCallback /*message_cb*/, + const CallbackRegistrationProto& callback_registration, Callback /*cb*/, + const SolveInterrupter* absl_nullable /*interrupter*/) { + OR_RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, + /*supported_events=*/{})); + OR_RETURN_IF_ERROR(CheckSolveParameters(parameters)); + + const absl::Time start = absl::Now(); + const SimpleMinCostFlow::Status status = mcf_->Solve(); + + OR_ASSIGN_OR_RETURN(SolveResultProto result, MakeSolveResult(status)); + OR_ASSIGN_OR_RETURN3(*result.mutable_solve_stats()->mutable_solve_time(), + util_time::EncodeGoogleApiProto(absl::Now() - start), + _ << "can't encode solve_time"); + return result; +} + +absl::StatusOr MinCostFlowSolver::Update(const ModelUpdateProto&) { + return false; +} + +absl::StatusOr +MinCostFlowSolver::ComputeInfeasibleSubsystem( + const SolveParametersProto& /*parameters*/, MessageCallback /*message_cb*/, + const SolveInterrupter* absl_nullable /*interrupter*/) { + return absl::UnimplementedError( + "MinCostFlow solver does not support ComputeInfeasibleSubsystem"); +} + +MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_MIN_COST_FLOW, MinCostFlowSolver::New); + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/min_cost_flow_solver.h b/ortools/math_opt/solvers/min_cost_flow_solver.h new file mode 100644 index 00000000000..b340766b3ba --- /dev/null +++ b/ortools/math_opt/solvers/min_cost_flow_solver.h @@ -0,0 +1,74 @@ +// 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_MATH_OPT_SOLVERS_MIN_COST_FLOW_SOLVER_H_ +#define ORTOOLS_MATH_OPT_SOLVERS_MIN_COST_FLOW_SOLVER_H_ + +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/statusor.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" +#include "ortools/graph/min_cost_flow.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research::math_opt { + +class MinCostFlowSolver : public SolverInterface { + public: + static absl::StatusOr> New( + const ModelProto& model, const InitArgs& init_args); + + absl::StatusOr Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters, + MessageCallback message_cb, + const CallbackRegistrationProto& callback_registration, Callback cb, + const SolveInterrupter* absl_nullable interrupter) override; + + absl::StatusOr Update(const ModelUpdateProto& model_update) override; + + absl::StatusOr + ComputeInfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + const SolveInterrupter* absl_nullable interrupter) override; + + private: + DEFINE_STRONG_INT_TYPE(ArcIndex, int32_t); + using VariableId = int64_t; + MinCostFlowSolver(std::unique_ptr mcf, + util_intops::StrongVector arc_to_var, + bool is_maximize, double objective_offset); + + absl::StatusOr MakeSolveResult( + SimpleMinCostFlow::Status status) const; + + const std::unique_ptr mcf_; + const util_intops::StrongVector arc_to_var_; + const bool is_maximize_; + const double objective_offset_; +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_SOLVERS_MIN_COST_FLOW_SOLVER_H_ diff --git a/ortools/math_opt/solvers/min_cost_flow_solver_test.cc b/ortools/math_opt/solvers/min_cost_flow_solver_test.cc new file mode 100644 index 00000000000..30962bbcc73 --- /dev/null +++ b/ortools/math_opt/solvers/min_cost_flow_solver_test.cc @@ -0,0 +1,594 @@ +// 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 +#include +#include +#include +#include + +#include "absl/base/attributes.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/types.h" +#include "ortools/graph/min_cost_flow.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/solver_tests/callback_tests.h" +#include "ortools/math_opt/solver_tests/generic_tests.h" +#include "ortools/math_opt/solver_tests/invalid_input_tests.h" +#include "ortools/math_opt/solver_tests/min_cost_flow_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" +#include "ortools/math_opt/testing/param_name.h" + +namespace operations_research::math_opt { +namespace { + +using ::testing::AllOf; +using ::testing::Field; +using ::testing::HasSubstr; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +// Tolerance is 1e-6 as defined in min_cost_flow_solver.cc. +constexpr double kMinCostFlowIntegralityTolerance = 1e-6; + +// Large floating point numbers for testing corner cases of converting to +// int64_t for cost values and flow quantities. +const double kLargeValidNumber = std::pow(2.0, 62.0); +const double kLargeInvalidNumber = std::pow(2.0, 63.0); + +static_assert(std::is_same_v, + "CostValue must be int64_t for these tests"); +static_assert(std::is_same_v, + "FlowQuantity must be int64_t for these tests"); + +// Generic tests. +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SimpleLogicalConstraintTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLogicalConstraintTest); + +INSTANTIATE_TEST_SUITE_P( + MinCostFlowGenericTest, GenericTest, + testing::Values(GenericTestParameters(SolverType::kMinCostFlow, + /*support_interrupter=*/false, + TestModelClass::kMinCostFlow, + /*expected_log=*/"", + /*solve_parameters=*/{}))); + +// MinCostFlow model tests. +INSTANTIATE_TEST_SUITE_P( + MinCostFlowSolverMinCostFlowTest, MinCostFlowTest, + testing::Values(MinCostFlowTestParams{ + .name = "min_cost_flow", + .solver_type = math_opt::SolverType::kMinCostFlow, + .lp_not_flow_error_substring = + "model structure is not supported by MinCostFlow solver", + .mip_not_flow_error_substring = "does not support integer variables", + .floating_point_cost_error_substring = "not close enough to an integer", + .floating_point_capacity_error_substring = + "not close enough to an integer", + .certifies_nontrivial_infeasibility = false, + .returns_dual_solution = false, + }), + ParamName{}); + +// The MinCostFlow solver does not support time limits. +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); +// This test should not be repeated for each solver, see b/172553545. +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(InvalidInputTest); + +INSTANTIATE_TEST_SUITE_P(MinCostFlowMessageCallbackTest, MessageCallbackTest, + testing::Values(MessageCallbackTestParams( + SolverType::kMinCostFlow, + /*support_message_callback=*/false, + /*support_interrupter=*/false, + /*integer_variables=*/false, + /*ending_substring=*/""))); + +INSTANTIATE_TEST_SUITE_P( + MinCostFlowCallbackTest, CallbackTest, + testing::Values(CallbackTestParams(SolverType::kMinCostFlow, + TestModelClass::kMinCostFlow, + /*add_lazy_constraints=*/false, + /*add_cuts=*/false, + /*supported_events=*/{}, + /*all_solutions=*/std::nullopt, + /*reaches_cut_callback=*/std::nullopt))); + +std::vector MinCostFlowBadParamTestCases() { + std::vector result; + for (const auto& [param, err] : + std::vector>({ + {{.time_limit = absl::Seconds(1)}, "time_limit"}, + {{.iteration_limit = 1}, "iteration_limit"}, + {{.node_limit = 1}, "node_limit"}, + {{.cutoff_limit = 1.0}, "cutoff_limit"}, + {{.objective_limit = 1.0}, "objective_limit"}, + {{.best_bound_limit = 1.0}, "best_bound_limit"}, + {{.solution_limit = 1}, "solution_limit"}, + {{.threads = 1}, "threads"}, + {{.random_seed = 1}, "random_seed"}, + {{.absolute_gap_tolerance = 1e-5}, "absolute_gap_tolerance"}, + {{.relative_gap_tolerance = 1e-5}, "relative_gap_tolerance"}, + {{.solution_pool_size = 1}, "solution_pool_size"}, + {{.lp_algorithm = LPAlgorithm::kPrimalSimplex}, "lp_algorithm"}, + {{.presolve = Emphasis::kOff}, "presolve"}, + {{.cuts = Emphasis::kOff}, "cuts"}, + {{.heuristics = Emphasis::kOff}, "heuristics"}, + {{.scaling = Emphasis::kOff}, "scaling"}, + })) { + result.push_back(InvalidParameterTestParams( + SolverType::kMinCostFlow, TestModelClass::kMinCostFlow, param, + {absl::StrCat("MinCostFlow solver does not support '", err, + "' parameter")})); + } + return result; +} + +INSTANTIATE_TEST_SUITE_P(MinCostFlowSolverTest, InvalidParameterTest, + testing::ValuesIn(MinCostFlowBadParamTestCases())); + +// Solver-specific tests. +TEST(MinCostFlowSolverTest, InvalidIntegerVariable) { + Model model; + const Variable x = model.AddIntegerVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("MinCostFlow does not support integer variables"))); +} + +TEST(MinCostFlowSolverTest, InvalidVariableLowerBound) { + Model model; + const Variable x = model.AddContinuousVariable(1.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable 0 lower bound is not 0"))); +} + +TEST(MinCostFlowSolverTest, InvalidInequalityConstraint) { + Model model; + const Variable x = model.AddContinuousVariable(1.0, 10.0); + model.AddLinearConstraint(9.0 <= x <= 10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("constraint 0 is not an equality"))); +} + +TEST(MinCostFlowSolverTest, VariableMissingNegativeOneCoefficient) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable 0 does not have a -1 coefficient"))); +} + +TEST(MinCostFlowSolverTest, VariableMissingPositiveOneCoefficient) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable 0 does not have a +1 coefficient"))); +} + +TEST(MinCostFlowSolverTest, MultipleNegativeOneCoefficients) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(-x == -10.0); + model.AddLinearConstraint(-x == -20.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable 0 has multiple -1 coefficients"))); +} + +TEST(MinCostFlowSolverTest, MultiplePositiveOneCoefficients) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(x == 20.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("variable 0 has multiple +1 coefficients"))); +} + +TEST(MinCostFlowSolverTest, ValidLargeCostValue) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 1.0); + model.AddLinearConstraint(x == 1.0); + model.AddLinearConstraint(-x == -1.0); + model.Minimize(kLargeValidNumber * x); + + // The cost value is accepted as valid in MinCostFlowSolver::New and passed + // down to SimpleMinCostFlow which returns "BAD_COST_RANGE" which is expected. + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(AllOf(TerminatesWith(TerminationReason::kOtherError), + Field("termination", &SolveResult::termination, + Field("detail", &Termination::detail, + HasSubstr("BAD_COST_RANGE")))))); +} + +TEST(MinCostFlowSolverTest, CostValueTooLarge) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(kLargeInvalidNumber * x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("is too large to be represented as a cost value"))); +} + +TEST(MinCostFlowSolverTest, ValidSmallCostValue) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 1.0); + model.AddLinearConstraint(x == 1.0); + model.AddLinearConstraint(-x == -1.0); + model.Minimize(-kLargeValidNumber * x); + + // The cost value is accepted as valid in MinCostFlowSolver::New and passed + // down to SimpleMinCostFlow which returns "BAD_COST_RANGE" which is expected. + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(AllOf(TerminatesWith(TerminationReason::kOtherError), + Field("termination", &SolveResult::termination, + Field("detail", &Termination::detail, + HasSubstr("BAD_COST_RANGE")))))); +} + +TEST(MinCostFlowSolverTest, CostValueTooSmall) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(-kLargeInvalidNumber * x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("is too small to be represented as a cost value"))); +} + +TEST(MinCostFlowSolverTest, ValidLargeFlowQuantity) { + Model model; + const Variable x = + model.AddContinuousVariable(0.0, std::numeric_limits::infinity()); + model.AddLinearConstraint(x == kLargeValidNumber); + model.AddLinearConstraint(-x == -kLargeValidNumber); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(IsOptimalWithSolution(kLargeValidNumber, + {{x, kLargeValidNumber}}))); +} + +TEST(MinCostFlowSolverTest, FlowQuantityTooLarge) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == kLargeInvalidNumber); + model.Minimize(x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("is too large to be represented as a flow quantity"))); +} + +TEST(MinCostFlowSolverTest, FlowQuantityTooSmall) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == -kLargeInvalidNumber); + model.Minimize(x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("is too small to be represented as a flow quantity"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalConstraintLowerBound) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(0.5 <= x <= 2.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid constraint 0 lower-bound"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalConstraintUpperBound) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(1.0 <= x <= 1.5); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid constraint 0 upper-bound"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalMatrixCoefficient) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(0.5 * x == 1.0); + model.Minimize(x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr("invalid coefficient for constraint 0 and variable 0"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalObjectiveCoefficient) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(0.5 * x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid objective coefficient for variable 0"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalVariableLowerBound) { + Model model; + const Variable x = model.AddContinuousVariable(0.5, 10.0); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid lower bound for variable 0"))); +} + +TEST(MinCostFlowSolverTest, InvalidFractionalVariableUpperBound) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.5); + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(-x == -10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid upper bound for variable 0"))); +} + +TEST(MinCostFlowSolverTest, VariableNotInConstraints) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.Minimize(x); + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("does not appear in any constraints"))); +} + +TEST(MinCostFlowSolverTest, InvalidQuadraticObjective) { + Model model; + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 10.0); + model.Minimize(x * x + 2.0 * x); + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("MinCostFlow does not support quadratic objectives"))); +} + +TEST(MinCostFlowSolverTest, ComputeInfeasibleSubsystemNotSupported) { + Model model; + EXPECT_THAT( + ComputeInfeasibleSubsystem(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("does not support ComputeInfeasibleSubsystem"))); +} + +TEST(MinCostFlowSolverTest, ConstraintBoundsDifferenceWithinTolerancePasses) { + Model model; + + const Variable x = model.AddContinuousVariable(0.0, 10.0); + const Variable y = model.AddContinuousVariable(0.0, 10.0); + + model.AddLinearConstraint(10.0 <= x <= + 10.0 + kMinCostFlowIntegralityTolerance); + model.AddLinearConstraint(y - x == 0.0); + model.AddLinearConstraint(-y == -10.0); + + model.Minimize(2.0 * x + 3.0 * y); + + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(IsOptimalWithSolution(50.0, {{x, 10.0}, {y, 10.0}}))); +} + +TEST(MinCostFlowSolverTest, ConstraintBoundsDifferenceMoreThanToleranceFails) { + Model model; + + const Variable x = model.AddContinuousVariable(0.0, 10.0); + const Variable y = model.AddContinuousVariable(0.0, 10.0); + + model.AddLinearConstraint(10.0 <= x <= + 10.0 + kMinCostFlowIntegralityTolerance + 1e-12); + model.AddLinearConstraint(y - x == 0.0); + model.AddLinearConstraint(-y == -10.0); + + model.Minimize(2.0 * x + 3.0 * y); + + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("not close enough to an integer"))); +} + +TEST(MinCostFlowSolverTest, + ConstraintBoundsDifferenceFromIntegerWithinTolerancePasses) { + Model model; + + const Variable x = model.AddContinuousVariable(0.0, 10.0); + const Variable y = model.AddContinuousVariable(0.0, 10.0); + + model.AddLinearConstraint(x == 10.0 + kMinCostFlowIntegralityTolerance); + model.AddLinearConstraint(y - x == 0.0); + model.AddLinearConstraint(-y == -10.0 - kMinCostFlowIntegralityTolerance); + + model.Minimize(2.0 * x + 3.0 * y); + + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(IsOptimalWithSolution(50.0, {{x, 10.0}, {y, 10.0}}))); +} + +TEST(MinCostFlowSolverTest, + ConstraintBoundsDifferenceFromIntegerMoreThanToleranceFails) { + Model model; + + const Variable x = model.AddContinuousVariable(0.0, 10.0); + const Variable y = model.AddContinuousVariable(0.0, 10.0); + + model.AddLinearConstraint(x == 10.0); + model.AddLinearConstraint(y - x == 0.0); + model.AddLinearConstraint(-y == + -10.0 + kMinCostFlowIntegralityTolerance + 1e-12); + + model.Minimize(2.0 * x + 3.0 * y); + + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("not close enough to an integer"))); +} + +// Test that an unbalanced problem is infeasible with a specific error message. +TEST(MinCostFlowSolverTest, UnbalancedProblem) { + Model model; + + const Variable x = model.AddContinuousVariable(0.0, 10.0); + model.AddLinearConstraint(x == 6.0); + model.AddLinearConstraint(-x == -5.0); + model.Minimize(x); + + EXPECT_THAT( + Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(AllOf(TerminatesWith(TerminationReason::kInfeasible), + Field("termination", &SolveResult::termination, + Field("detail", &Termination::detail, + HasSubstr("problem is unbalanced")))))); +} + +// A valid Min-Cost Flow problem formulated as a MIP with unbounded arcs and +// large supply and cost values. +// +// Nodes A(supply=int32_max), B(supply=-5), C(supply=-(int32_max - 5)) +// Arcs: +// A->B (capacity=int32_max - 1, cost=2e8) +// B->C (capacity=inf, cost=3e8) +// A->C (capacity=inf, cost=5e8 + 1) +TEST(MinCostFlowSolverTest, ValidProblemWithLargeNumbers) { + Model model; + + const double int32_max = kint32max; + + // Create arcs with large and infinite capacities. + const Variable ab = model.AddContinuousVariable(0.0, int32_max - 1, "ab"); + const Variable bc = model.AddContinuousVariable( + 0.0, std::numeric_limits::infinity(), "bc"); + const Variable ac = model.AddContinuousVariable( + 0.0, std::numeric_limits::infinity(), "ac"); + + // Create flow conservation constraints: outflow - inflow = supply + model.AddLinearConstraint(ab + ac == int32_max, "NodeA"); + model.AddLinearConstraint(bc - ab == -5.0, "NodeB"); + model.AddLinearConstraint(-bc - ac == -(int32_max - 5.0), "NodeC"); + + // Objective: Minimize 2e8 * ab + 3e8 * bc + (5e8 + 1) * ac + model.Minimize(2e8 * ab + 3e8 * bc + (5e8 + 1) * ac); + + // Optimal flow: + // A must send `int32_max` units of flow. The path A->B->C costs + // 2e8 + 3e8 = 5e8 per unit, which is slightly cheaper than A->C (cost + // 5e8 + 1). After saturating A->B (capacity `int32_max - 1`), A sends the + // remaining 1 unit via A->C. + // B consumes 5 units (supply -5 = demand 5). B forwards the remaining + // `int32_max - 1 - 5 = int32_max - 6` units to C via B->C. C receives + // `int32_max - 6` units from B and 1 unit from A, satisfying its demand of + // `int32_max - 5`. + // Costs: + // ab: (int32_max - 1) * 2e8 + // bc: (int32_max - 6) * 3e8 + // ac: 1 unit * (5e8 + 1) = 5e8 + 1 + // Total cost: 5e8 * int32_max - 1.5e9 + 1 + EXPECT_THAT(Solve(model, SolverType::kMinCostFlow), + IsOkAndHolds(IsOptimalWithSolution( + 5e8 * int32_max - 1.5e9 + 1.0, + {{ab, int32_max - 1.0}, {bc, int32_max - 6.0}, {ac, 1.0}}))); +} + +// Test that objective value is consistent for nontrivial models. +struct NontrivialModelParams { + int num_nodes ABSL_REQUIRE_EXPLICIT_INIT; + int seed ABSL_REQUIRE_EXPLICIT_INIT; +}; + +std::ostream& operator<<(std::ostream& out, + const NontrivialModelParams& params) { + out << "{ num_nodes: " << params.num_nodes << ", seed: " << params.seed + << " }"; + return out; +} + +class NontrivialModelTest + : public ::testing::TestWithParam {}; + +TEST_P(NontrivialModelTest, ObjectiveValueIsConsistent) { + const NontrivialModelParams& params = GetParam(); + const std::unique_ptr model = NontrivialModel( + TestModelClass::kMinCostFlow, params.num_nodes, params.seed); + + ASSERT_OK_AND_ASSIGN(const SolveResult glop_result, + Solve(*model, SolverType::kGlop)); + ASSERT_THAT(glop_result, IsOptimal()); + + // We allow a small tolerance to account for floating point inaccuracies in + // Glop. + EXPECT_THAT(Solve(*model, SolverType::kMinCostFlow), + IsOkAndHolds(IsOptimal(glop_result.objective_value(), 1.0e-6))); +} + +INSTANTIATE_TEST_SUITE_P( + MinCostFlowSolverTest, NontrivialModelTest, + testing::Values(NontrivialModelParams{.num_nodes = 10, .seed = 1}, + NontrivialModelParams{.num_nodes = 10, .seed = 2}, + NontrivialModelParams{.num_nodes = 50, .seed = 3}, + NontrivialModelParams{.num_nodes = 50, .seed = 4}, + NontrivialModelParams{.num_nodes = 100, .seed = 5}, + NontrivialModelParams{.num_nodes = 100, .seed = 6}, + NontrivialModelParams{.num_nodes = 200, .seed = 7}, + NontrivialModelParams{.num_nodes = 200, .seed = 8}), + [](const testing::TestParamInfo& info) { + return absl::StrCat(info.param.num_nodes, "_", info.param.seed); + }); + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/pdlp_solver_test.cc b/ortools/math_opt/solvers/pdlp_solver_test.cc index af179dce4b3..db98a548d4a 100644 --- a/ortools/math_opt/solvers/pdlp_solver_test.cc +++ b/ortools/math_opt/solvers/pdlp_solver_test.cc @@ -33,12 +33,15 @@ #include "ortools/math_opt/solver_tests/lp_model_solve_parameters_tests.h" #include "ortools/math_opt/solver_tests/lp_parameter_tests.h" #include "ortools/math_opt/solver_tests/lp_tests.h" +#include "ortools/math_opt/solver_tests/min_cost_flow_tests.h" #include "ortools/math_opt/solver_tests/multi_objective_tests.h" #include "ortools/math_opt/solver_tests/qc_tests.h" #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/testing/param_name.h" #include "ortools/pdlp/solve_log.pb.h" namespace operations_research { @@ -70,6 +73,20 @@ INSTANTIATE_TEST_SUITE_P(PdlpSimpleLpTest, SimpleLpTest, /*ensures_dual_ray=*/true, /*disallows_infeasible_or_unbounded=*/false))); +INSTANTIATE_TEST_SUITE_P( + PdlpMinCostFlowTest, MinCostFlowTest, + testing::Values(MinCostFlowTestParams{ + .name = "pdlp", + .solver_type = math_opt::SolverType::kPdlp, + .lp_not_flow_error_substring = std::nullopt, + .mip_not_flow_error_substring = "integer variables", + .floating_point_cost_error_substring = std::nullopt, + .floating_point_capacity_error_substring = std::nullopt, + .certifies_nontrivial_infeasibility = true, + .returns_dual_solution = true, + }), + ParamName{}); + MultiObjectiveTestParameters GetPdlpMultiObjectiveTestParameters() { return MultiObjectiveTestParameters( /*solver_type=*/SolverType::kPdlp, /*parameters=*/SolveParameters(), @@ -148,8 +165,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(PdlpInvalidInputTest, InvalidInputTest, testing::Values(InvalidInputTestParameters( - SolverType::kPdlp, - /*use_integer_variables=*/false))); + SolverType::kPdlp, TestModelClass::kLp))); INSTANTIATE_TEST_SUITE_P( PdlpLpParameterTest, LpParameterTest, @@ -168,7 +184,7 @@ InvalidParameterTestParams MakeBadPdlpSpecificParams() { SolveParameters parameters; parameters.pdlp.set_major_iteration_frequency(-7); return InvalidParameterTestParams( - SolverType::kPdlp, std::move(parameters), + SolverType::kPdlp, TestModelClass::kLp, std::move(parameters), {"major_iteration_frequency must be positive"}); } @@ -177,7 +193,7 @@ InvalidParameterTestParams MakeBadCommonParamsForPdlp() { parameters.cuts = Emphasis::kHigh; parameters.lp_algorithm = LPAlgorithm::kDualSimplex; return InvalidParameterTestParams( - SolverType::kPdlp, std::move(parameters), + SolverType::kPdlp, TestModelClass::kLp, std::move(parameters), /*expected_error_substrings=*/ {"parameter cuts not supported for PDLP", "parameter lp_algorithm not supported for PDLP"}); @@ -210,8 +226,7 @@ INSTANTIATE_TEST_SUITE_P( PdlpGenericTest, GenericTest, testing::Values(GenericTestParameters( SolverType::kPdlp, - /*support_interrupter=*/true, - /*integer_variables=*/false, + /*support_interrupter=*/true, TestModelClass::kLp, /*expected_log=*/"Termination reason: TERMINATION_REASON_OPTIMAL"))); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); @@ -228,8 +243,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( PdlpCallbackTest, CallbackTest, - testing::Values(CallbackTestParams(SolverType::kPdlp, - /*integer_variables=*/false, + testing::Values(CallbackTestParams(SolverType::kPdlp, TestModelClass::kLp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, diff --git a/ortools/math_opt/solvers/xpress.proto b/ortools/math_opt/solvers/xpress.proto index c83dba3e6d8..ae9d1a2bb8a 100644 --- a/ortools/math_opt/solvers/xpress.proto +++ b/ortools/math_opt/solvers/xpress.proto @@ -22,7 +22,18 @@ option java_outer_classname = "Xpress"; // Parameters used to initialize the Xpress solver. message XpressInitializerProto { - optional bool extract_names = 1; + message License { + // If provided, Xpress will use load the license from this path. If not + // provided, Xpress will look for xpauth.xpr in default locations. + // See + // https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/XPRSinit.html + // for details. + string path = 1; + } + + // The license to use. If not provided, Xpress will look for xpauth.xpr in + // default locations. + optional License license = 1; } // Xpress specific parameters for solving. See diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 6261a9aac50..65e017ff2ca 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -94,13 +94,12 @@ absl::Status Xpress::RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); } -absl::Status Xpress::AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata, int prio) { +absl::Status Xpress::AddCbChecktime(ChecktimeCallback cb, void* cbdata, + int prio) { return ToStatus(XPRSaddcbchecktime(xpress_model_, cb, cbdata, prio)); } -absl::Status Xpress::RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata) { +absl::Status Xpress::RemoveCbChecktime(ChecktimeCallback cb, void* cbdata) { return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata)); } @@ -164,7 +163,7 @@ absl::Status Xpress::AddVars(std::size_t count, // Since we don't add any non-zeros here, it is safe to use XPRSaddcols(). OR_RETURN_IF_ERROR(ToStatus(XPRSaddcols( xpress_model_, num_vars, 0, c_obj, nullptr, nullptr, nullptr, - lb.size() ? lb.data() : nullptr, ub.size() ? ub.data() : nullptr))); + lb.empty() ? nullptr : lb.data(), ub.empty() ? nullptr : ub.data()))); if (!vtype.empty()) { for (int i = 0; i < num_vars; ++i) colind.push_back(oldCols + i); int const ret = @@ -572,9 +571,9 @@ absl::Status Xpress::AddRows(absl::Span rowtype, rowtype.size() != start.size() || colind.size() != rowcoef.size()) return absl::InvalidArgumentError("inconsistent arguments to AddRows"); return ToStatus(XPRSaddrows64(xpress_model_, static_cast(rowtype.size()), - colind.size(), rowtype.data(), rhs.data(), - rng.data(), start.data(), colind.data(), - rowcoef.data())); + static_cast(rowcoef.size()), + rowtype.data(), rhs.data(), rng.data(), + start.data(), colind.data(), rowcoef.data())); } absl::Status Xpress::AddQRow(char sense, double rhs, double rng, @@ -587,12 +586,17 @@ absl::Status Xpress::AddQRow(char sense, double rhs, double rng, if (checkInt32Overflow(std::size_t(oldRows) + 1)) return absl::InvalidArgumentError( "XPRESS cannot handle more than 2^31 rows"); + if (colind.size() != rowcoef.size() || qcol1.size() != qcol2.size() || + qcol1.size() != qcoef.size()) + return absl::InvalidArgumentError("inconsistent arguments to AddQRows"); XPRSint64 const start = 0; + const int num_coefs = static_cast(rowcoef.size()); OR_RETURN_IF_ERROR( - ToStatus(XPRSaddrows64(xpress_model_, 1, colind.size(), &sense, &rhs, - &rng, &start, colind.data(), rowcoef.data()))); - if (qcol1.size() > 0) { - int const ret = XPRSaddqmatrix64(xpress_model_, oldRows, qcol1.size(), + ToStatus(XPRSaddrows64(xpress_model_, 1, num_coefs, &sense, &rhs, &rng, + &start, colind.data(), rowcoef.data()))); + if (!qcol1.empty()) { + const int num_qcoefs = static_cast(qcoef.size()); + int const ret = XPRSaddqmatrix64(xpress_model_, oldRows, num_qcoefs, qcol1.data(), qcol2.data(), qcoef.data()); if (ret != 0) { XPRSdelrows(xpress_model_, 1, &oldRows); @@ -630,8 +634,8 @@ absl::Status Xpress::ChgBounds(absl::Span colind, if (checkInt32Overflow(colind.size())) return absl::InvalidArgumentError( "XPRESS cannot handle more than 2^31 bound changes"); - return ToStatus(XPRSchgbounds(xpress_model_, colind.size(), colind.data(), - bndtype.data(), bndval.data())); + return ToStatus(XPRSchgbounds(xpress_model_, static_cast(colind.size()), + colind.data(), bndtype.data(), bndval.data())); } absl::Status Xpress::ChgColType(absl::Span colind, absl::Span coltype) { @@ -640,8 +644,8 @@ absl::Status Xpress::ChgColType(absl::Span colind, if (checkInt32Overflow(colind.size())) return absl::InvalidArgumentError( "XPRESS cannot handle more than 2^31 type changes"); - return ToStatus(XPRSchgcoltype(xpress_model_, colind.size(), colind.data(), - coltype.data())); + return ToStatus(XPRSchgcoltype(xpress_model_, static_cast(colind.size()), + colind.data(), coltype.data())); } } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 64ce3a3f7e3..c6cc30501b9 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -136,10 +136,10 @@ class Xpress { absl::Status RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata = nullptr); - absl::Status AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), void* cbdata, - int prio = 0); - absl::Status RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata = nullptr); + + using ChecktimeCallback = int(XPRS_CC*)(XPRSprob, void*); + absl::Status AddCbChecktime(ChecktimeCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbChecktime(ChecktimeCallback cb, void* cbdata = nullptr); absl::StatusOr> GetVarLb() const; absl::StatusOr> GetVarUb() const; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 0f7c33903cf..e54bfba0735 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -14,39 +14,54 @@ #include "ortools/math_opt/solvers/xpress_solver.h" #include +#include +#include #include +#include +#include +#include #include #include #include -#include #include +#include #include +#include "absl/base/config.h" // IWYU pragma: keep +#include "absl/base/nullability.h" +#include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_set.h" #include "absl/container/linked_hash_map.h" #include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" +#include "absl/synchronization/mutex.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "absl/types/span.h" +#include "google/protobuf/map.h" #include "ortools/base/map_util.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" +#include "ortools/base/types.h" #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" -#include "ortools/math_opt/cpp/math_opt.h" -#include "ortools/math_opt/cpp/streamable_solver_init_arguments.h" #include "ortools/math_opt/solvers/xpress/g_xpress.h" #include "ortools/math_opt/validators/callback_validator.h" -#include "ortools/port/proto_utils.h" #include "ortools/third_party_solvers/xpress_environment.h" #include "ortools/util/solve_interrupter.h" +#ifdef ABSL_HAVE_EXCEPTIONS +#include +#endif + namespace operations_research { namespace math_opt { namespace { @@ -54,37 +69,35 @@ namespace { struct SharedSolveContext { Xpress* xpress; - /** Mutex for accessing callbackException. */ + /** Mutex for accessing callback_status_. */ absl::Mutex mutex; /** Capturing of exceptions in callbacks. * We cannot let exceptions escape from callbacks since that would just * unroll the stack until some function that catches the exception. * In particular, it would bypass any cleanup code implemented in the C code - * of the solver. So we must capture exceptions, interrupt the solve and - * handle the exception once the solver returned. + * of the solver. So we must capture exceptions, convert them to Status, + * interrupt the solve and handle the status once the solver returned. */ - std::exception_ptr callbackException; + absl::Status callback_status ABSL_GUARDED_BY(mutex) = absl::OkStatus(); }; /** Registered callback that is auto-removed in the destructor. * Use Add() to add a callback to a solve context. - * The class also provides convenience functions SetCallbackException() + * The class also provides convenience functions SetCallbackStatus() * and Interrupt() that are required in every callback implementation to - * capture exceptions from user code and reraise them appropriately. + * capture exceptions from user code and return them as Status. */ template class ScopedCallback { using proto_type = typename ProtoT::proto_type; - SharedSolveContext* ctx; + SharedSolveContext* ctx_; ScopedCallback(ScopedCallback const&) = delete; - ScopedCallback(ScopedCallback&&) = delete; ScopedCallback& operator=(ScopedCallback const&) = delete; - ScopedCallback& operator=(ScopedCallback&&) = delete; // We intercept and store any exception throw by a callback defining a static - // wrapper function that invokes the callback within a try/carch block. For + // wrapper function that invokes the callback within a try/catch block. For // this to work, we need to deduce the callback return type and arguments. template struct ExWrapper; @@ -93,43 +106,58 @@ class ScopedCallback { template struct ExWrapper { // The static function that will be directly invoked by Xpress - static auto low_level_cb(XPRSprob prob, void* cbdata, Args... args) try { - return ProtoT::glueFn(prob, cbdata, args...); - } catch (...) { - // Catch any exception and terminate Xpress gracefully - ScopedCallback* cb = reinterpret_cast(cbdata); - cb->Interrupt(XPRS_STOP_USER); - cb->SetCallbackException(std::current_exception()); - if constexpr (std::is_convertible_v) return static_cast(1); + static auto low_level_cb(XPRSprob prob, void* cbdata, Args... args) { +#ifdef ABSL_HAVE_EXCEPTIONS + try { +#endif + return ProtoT::glueFn(prob, cbdata, args...); +#ifdef ABSL_HAVE_EXCEPTIONS + } catch (const std::exception& e) { + // Catch any exception and terminate Xpress gracefully + ScopedCallback* cb = reinterpret_cast(cbdata); + cb->Interrupt(XPRS_STOP_USER); + cb->SetCallbackStatus(absl::UnknownError(e.what())); + if constexpr (std::is_convertible_v) return static_cast(1); + } catch (...) { + // Catch any exception and terminate Xpress gracefully + ScopedCallback* cb = reinterpret_cast(cbdata); + cb->Interrupt(XPRS_STOP_USER); + cb->SetCallbackStatus(absl::UnknownError( + "a C++ exception that is not a std::exception occurred in " + "Xpress callback")); + if constexpr (std::is_convertible_v) return static_cast(1); + } +#endif } }; - const proto_type low_level_cb = ExWrapper::low_level_cb; + const proto_type low_level_cb_ = ExWrapper::low_level_cb; public: - CbT or_tools_cb; + CbT or_tools_cb_; - ScopedCallback() : ctx(nullptr) {} + ScopedCallback() : ctx_(nullptr) {} inline absl::Status Add(SharedSolveContext* context, CbT cb) { - ctx = context; - OR_RETURN_IF_ERROR( - ProtoT::Add(ctx->xpress, low_level_cb, reinterpret_cast(this))); - or_tools_cb = cb; + ctx_ = context; + OR_RETURN_IF_ERROR(ProtoT::Add(ctx_->xpress, low_level_cb_, + reinterpret_cast(this))); + or_tools_cb_ = cb; return absl::OkStatus(); } inline void Interrupt(int reason) { - CHECK_OK(ctx->xpress->Interrupt(reason)); + CHECK_OK(ctx_->xpress->Interrupt(reason)); } - inline void SetCallbackException(std::exception_ptr ex) { - const absl::MutexLock lock(&ctx->mutex); - if (!ctx->callbackException) ctx->callbackException = ex; + inline void SetCallbackStatus(const absl::Status& status) { + const absl::MutexLock lock(ctx_->mutex); + ctx_->callback_status.Update(status); } ~ScopedCallback() { - if (ctx) - ProtoT::Remove(ctx->xpress, low_level_cb, reinterpret_cast(this)); + if (ctx_) + ProtoT::Remove(ctx_->xpress, low_level_cb_, + reinterpret_cast(this)); } }; @@ -167,20 +195,23 @@ class ScopedCallback { /** Define the message callback. * This forwards messages from Xpress to an ortools message callback. */ -DEFINE_SCOPED_CB(Message, MessageCallback, void, - (XPRSprob prob, void* cbdata, char const* msg, int len, +DEFINE_SCOPED_CB(Message, SolverInterface::MessageCallback, void, + (XPRSprob /*prob*/, void* cbdata, char const* msg, int len, int type)) { auto cb = reinterpret_cast(cbdata); - if (type != 1 && // info message - type != 3 && // warning message - type != 4) { // error message - // message type 2 is not used by Xpress, negative values mean "flush" - return; + switch (type) { + case 1: // info message + case 3: // warning message + case 4: // error message + break; + default: + // message type 2 is not used by Xpress, negative values mean "flush" + return; } if (len == 0) { - cb->or_tools_cb(std::vector{""}); + cb->or_tools_cb_(std::vector{""}); return; } @@ -201,20 +232,20 @@ DEFINE_SCOPED_CB(Message, MessageCallback, void, } start = end + 1; } - cb->or_tools_cb(lines); + cb->or_tools_cb_(lines); } /** Define the checktime callback. * This callbacks checks an interrupter for whether the solve was interrupted. */ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, - (XPRSprob prob, void* cbdata)) { + (XPRSprob /*prob*/, void* cbdata)) { auto cb = reinterpret_cast(cbdata); // Note: we do NOT return non-zero from the callback if the solve was // interrupted. Returning non-zero from the callback is interpreted // as hitting a time limit and we would therefore not map correctly // the resulting stop status to ortools' termination status. - if (cb->or_tools_cb->IsInterrupted()) { + if (cb->or_tools_cb_->IsInterrupted()) { cb->Interrupt(XPRS_STOP_USER); } return 0; @@ -271,13 +302,13 @@ inline int MathOptToXpressBasisStatus(const BasisStatusProto status, */ class ScopedSolverContext { /** Solver context data shared by callbacks */ - SharedSolveContext shared_ctx; + SharedSolveContext shared_ctx_; /** Installed message callback (if any). */ - MessageScopedCb messageCallback; + MessageScopedCb message_callback_; /** Installed interrupter (if any). */ - ChecktimeScopedCb checktimeCallback; + ChecktimeScopedCb checktime_callback_; /** If we installed an interrupter callback then this removes it. */ - std::function removeInterrupterCallback; + std::unique_ptr interrupter_callback_; /** A single control that must be reset in the destructor. */ struct OneControl { int id; @@ -289,73 +320,66 @@ class ScopedSolverContext { }; // Matches std::variant<>::index; }; /** Controls to be reset in the destructor. */ - std::vector modifiedControls; + std::vector modified_controls_; public: - ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { - shared_ctx.xpress = xpress; + explicit ScopedSolverContext(Xpress* xpress) { shared_ctx_.xpress = xpress; } + absl::Status Set(int id, int32_t value) { + return Set(id, static_cast(value)); } - absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); } absl::Status Set(int id, int64_t value) { - OR_ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); - modifiedControls.push_back({id, old}); - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); + OR_ASSIGN_OR_RETURN(int64_t old, shared_ctx_.xpress->GetIntControl64(id)); + modified_controls_.push_back({id, old}); + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetIntControl64(id, value)); return absl::OkStatus(); } absl::Status Set(int id, double value) { - OR_ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id)); - modifiedControls.push_back({id, old}); - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value)); + OR_ASSIGN_OR_RETURN(double old, shared_ctx_.xpress->GetDblControl(id)); + modified_controls_.push_back({id, old}); + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetDblControl(id, value)); return absl::OkStatus(); } absl::Status Set(int id, std::string const& value) { - OR_ASSIGN_OR_RETURN(std::string old, shared_ctx.xpress->GetStrControl(id)); - modifiedControls.push_back({id, old}); - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetStrControl(id, value)); + OR_ASSIGN_OR_RETURN(std::string old, shared_ctx_.xpress->GetStrControl(id)); + modified_controls_.push_back({id, old}); + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetStrControl(id, value)); return absl::OkStatus(); } - absl::Status AddCallbacks(MessageCallback message_callback, + absl::Status AddCallbacks(SolverInterface::MessageCallback message_callback, const SolveInterrupter* interrupter) { if (message_callback) - OR_RETURN_IF_ERROR(messageCallback.Add(&shared_ctx, message_callback)); + OR_RETURN_IF_ERROR(message_callback_.Add(&shared_ctx_, message_callback)); if (interrupter) { /* To be extra safe we add two ways to interrupt Xpress: * 1. We register a checktime callback that polls the interrupter. * 2. We register a callback with the interrupter that will call * XPRSinterrupt(). * Eventually we should assess whether the first thing is a performance - * hit and if so, remove it. - */ - OR_RETURN_IF_ERROR(checktimeCallback.Add(&shared_ctx, interrupter)); - SolveInterrupter::CallbackId const id = - interrupter->AddInterruptionCallback( - [=] { CHECK_OK(shared_ctx.xpress->Interrupt(XPRS_STOP_USER)); }); - removeInterrupterCallback = [=] { - interrupter->RemoveInterruptionCallback(id); - }; - /** TODO: Support - * CallbackRegistrationProto and Callback and install the - * ortools callback as required. - * Note that this is only for Solve(), not for - * ComputeInfeasibleSubsystem() - */ + * hit and if so, remove it. */ + OR_RETURN_IF_ERROR(checktime_callback_.Add(&shared_ctx_, interrupter)); + interrupter_callback_ = std::make_unique( + interrupter, + [this] { CHECK_OK(shared_ctx_.xpress->Interrupt(XPRS_STOP_USER)); }); + /** @TODO Support CallbackRegistrationProto and Callback and install the + * ortools callback as required. Note that this is only for Solve(), not + * for ComputeInfeasibleSubsystem() */ } return absl::OkStatus(); } /** Setup model specific parameters. */ - absl::Status ApplyParameters(const SolveParametersProto& parameters, - MessageCallback message_callback, - std::string* export_model, bool* force_postsolve, - bool* stop_after_lp) { + absl::Status ApplyParameters( + const SolveParametersProto& parameters, + SolverInterface::MessageCallback message_callback, + std::string* export_model, bool* force_postsolve, bool* stop_after_lp) { std::vector warnings; - OR_ASSIGN_OR_RETURN(bool const isMIP, shared_ctx.xpress->IsMIP()); + OR_ASSIGN_OR_RETURN(bool const isMIP, shared_ctx_.xpress->IsMIP()); if (parameters.enable_output()) { // This is considered only if no message callback is set, see the // ortools specification of the enable_output parameter. if (!message_callback) { OR_RETURN_IF_ERROR( - messageCallback.Add(&shared_ctx, stdoutMessageCallback)); + message_callback_.Add(&shared_ctx_, stdoutMessageCallback)); } } absl::Duration time_limit = absl::InfiniteDuration(); @@ -433,6 +457,10 @@ class ScopedSolverContext { break; // Note: Xpress also supports network simplex, but that is not // supported by ortools. + default: + LOG(FATAL) << "LPAlgorithm: " + << LPAlgorithmProto_Name(parameters.lp_algorithm()) + << " unknown, error setting Xpress parameters"; } } if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { @@ -454,6 +482,10 @@ class ScopedSolverContext { case EMPHASIS_VERY_HIGH: presolvePasses = 5; break; + default: + LOG(FATAL) << "Presolve emphasis: " + << EmphasisProto_Name(parameters.presolve()) + << " unknown, error setting Xpress parameters"; } if (presolvePasses > 0) OR_RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolvePasses)); @@ -475,6 +507,10 @@ class ScopedSolverContext { case EMPHASIS_VERY_HIGH: OR_RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); // Same as high break; + default: + LOG(FATAL) << "Cuts emphasis: " + << EmphasisProto_Name(parameters.cuts()) + << " unknown, error setting Xpress parameters"; } } if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { @@ -492,6 +528,10 @@ class ScopedSolverContext { case EMPHASIS_VERY_HIGH: OR_RETURN_IF_ERROR(Set(XPRS_HEUREMPHASIS, 2)); break; + default: + LOG(FATAL) << "Heuristics emphasis: " + << EmphasisProto_Name(parameters.heuristics()) + << " unknown, error setting Xpress parameters"; } } @@ -522,7 +562,7 @@ class ScopedSolverContext { continue; } OR_RETURN_IF_ERROR( - shared_ctx.xpress->GetControlInfo(name.c_str(), &id, &type)); + shared_ctx_.xpress->GetControlInfo(name.c_str(), &id, &type)); switch (type) { case XPRS_TYPE_INT: // fallthrough case XPRS_TYPE_INT64: @@ -530,8 +570,7 @@ class ScopedSolverContext { return ortools::InvalidArgumentErrorBuilder() << "value " << value << " for " << name << " is not an integer"; - if (type == XPRS_TYPE_INT && (l > std::numeric_limits::max() || - l < std::numeric_limits::min())) + if (type == XPRS_TYPE_INT && (l > kint32max || l < kint32min)) return ortools::InvalidArgumentErrorBuilder() << "value " << value << " for " << name << " is out of range"; @@ -570,16 +609,16 @@ class ScopedSolverContext { XpressSolver::XpressMultiObjectiveIndex> const& objectives_map) { OR_ASSIGN_OR_RETURN(int const cols, - shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALCOLS)); + shared_ctx_.xpress->GetIntAttr(XPRS_ORIGINALCOLS)); OR_ASSIGN_OR_RETURN(int const rows, - shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALROWS)); + shared_ctx_.xpress->GetIntAttr(XPRS_ORIGINALROWS)); // Set initial basis if (model_parameters.has_initial_basis()) { // XPRSloadbasis() will raise an error if called on a model in presolved // state. We still trap this already here so that we can produce a more // meaningful error message. OR_ASSIGN_OR_RETURN(int const state, - shared_ctx.xpress->GetIntAttr(XPRS_PRESOLVESTATE)); + shared_ctx_.xpress->GetIntAttr(XPRS_PRESOLVESTATE)); if (state & ((1 << 1) | (1 << 2))) { return ortools::InvalidArgumentErrorBuilder() << "cannot set basis for model in presolved space (consider " @@ -599,7 +638,7 @@ class ScopedSolverContext { MathOptToXpressBasisStatus(static_cast(value), true); } - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetStartingBasis( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetStartingBasis( xpress_constr_basis_status, xpress_var_basis_status)); } std::vector colind; @@ -623,7 +662,7 @@ class ScopedSolverContext { return ortools::InvalidArgumentErrorBuilder() << "more solution hints than columns"; // XPRSaddmipsol() expects a solution in the original space - OR_RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->AddMIPSol( mipStart, colind, absl::StrCat("SolutionHint", cnt).c_str())); ++cnt; } @@ -648,7 +687,7 @@ class ScopedSolverContext { 1000 - prio); // Smaller prios have higher precedence in Xpress! } - OR_RETURN_IF_ERROR(shared_ctx.xpress->LoadDirs( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->LoadDirs( absl::MakeSpan(colind), absl::MakeSpan(priority), std::nullopt, std::nullopt, std::nullopt)); } @@ -660,25 +699,25 @@ class ScopedSolverContext { // multi-objective models. We just set them blindly here. They don't // hurt for a single-objective model. if (p.has_objective_degradation_absolute_tolerance()) { - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( 0, XPRS_OBJECTIVE_ABSTOL, p.objective_degradation_absolute_tolerance())); } if (p.has_objective_degradation_relative_tolerance()) { - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( 0, XPRS_OBJECTIVE_RELTOL, p.objective_degradation_relative_tolerance())); } if (p.has_time_limit()) { // We support a time limit but only if there is one single objective. - if (objectives_map.size() > 0) { + if (!objectives_map.empty()) { return ortools::InvalidArgumentErrorBuilder() << "Xpress does not support per-objective time limits"; } OR_ASSIGN_OR_RETURN(auto l, util_time::DecodeGoogleApiProto(p.time_limit())); - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetDblControl( XPRS_TIMELIMIT, absl::ToDoubleSeconds(l))); } } @@ -686,12 +725,12 @@ class ScopedSolverContext { for (auto const& [id, p] : model_parameters.auxiliary_objective_parameters()) { if (p.has_objective_degradation_absolute_tolerance()) { - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( objectives_map.at(id), XPRS_OBJECTIVE_ABSTOL, p.objective_degradation_absolute_tolerance())); } if (p.has_objective_degradation_relative_tolerance()) { - OR_RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + OR_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( objectives_map.at(id), XPRS_OBJECTIVE_RELTOL, p.objective_degradation_relative_tolerance())); } @@ -711,44 +750,41 @@ class ScopedSolverContext { return ortools::InvalidArgumentErrorBuilder() << "more lazy constraints than rows"; - OR_RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows(delayedRows)); + OR_RETURN_IF_ERROR(shared_ctx_.xpress->LoadDelayedRows(delayedRows)); } return absl::OkStatus(); } /** Interrupt the current solve with the given reason. */ - void Interrupt(int reason) { CHECK_OK(shared_ctx.xpress->Interrupt(reason)); } + void Interrupt(int reason) { + CHECK_OK(shared_ctx_.xpress->Interrupt(reason)); + } - void ReraiseException() { - if (shared_ctx.callbackException) { - std::exception_ptr ex = shared_ctx.callbackException; - shared_ctx.callbackException = nullptr; - std::rethrow_exception(ex); - } + absl::Status GetCallbackStatus() { + const absl::MutexLock lock(shared_ctx_.mutex); + return shared_ctx_.callback_status; } ~ScopedSolverContext() { - for (auto it = modifiedControls.rbegin(); it != modifiedControls.rend(); + for (auto it = modified_controls_.rbegin(); it != modified_controls_.rend(); ++it) { switch (it->value.index()) { case OneControl::INT_CONTROL: - CHECK_OK(shared_ctx.xpress->SetIntControl64( + CHECK_OK(shared_ctx_.xpress->SetIntControl64( it->id, std::get(it->value))); break; case OneControl::DBL_CONTROL: - CHECK_OK(shared_ctx.xpress->SetDblControl( + CHECK_OK(shared_ctx_.xpress->SetDblControl( it->id, std::get(it->value))); break; case OneControl::STR_CONTROL: - CHECK_OK(shared_ctx.xpress->SetStrControl( - it->id, std::get(it->value).c_str())); + CHECK_OK(shared_ctx_.xpress->SetStrControl( + it->id, std::get(it->value))); break; } } - if (removeInterrupterCallback) removeInterrupterCallback(); // If pending callback exception was not reraised yet then do it now - if (shared_ctx.callbackException) - std::rethrow_exception(shared_ctx.callbackException); + CHECK_OK(shared_ctx_.callback_status); } }; @@ -851,7 +887,7 @@ struct NameResolver { template struct NameResolver> { static std::string const& GetName( - google::protobuf::Map const& container, + google::protobuf::Map const& /*container*/, typename google::protobuf::Map::const_iterator const& i) { return i->second.name(); } @@ -880,7 +916,7 @@ absl::Status AddNames(Xpress* xpress, int type, int offset, I begin, I end, ++i; ++begin; } - if (buffer.size()) { + if (!buffer.empty()) { OR_RETURN_IF_ERROR( xpress->AddNames(type, buffer, offset + start, offset + i - 1)); } @@ -908,7 +944,7 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .indicator_constraints = SupportType::kSupported}; absl::StatusOr> XpressSolver::New( - const ModelProto& model, const InitArgs& init_args) { + const ModelProto& model, const InitArgs& /*init_args*/) { if (!XpressIsCorrectlyInstalled()) { return absl::InvalidArgumentError("Xpress is not correctly installed."); } @@ -919,11 +955,7 @@ absl::StatusOr> XpressSolver::New( // (for example, if XPRESS does not support multi-objective with quad terms) OR_ASSIGN_OR_RETURN(auto xpr, Xpress::New(model.name())); - bool extract_names = init_args.streamable.has_xpress() && - init_args.streamable.xpress().has_extract_names() && - init_args.streamable.xpress().extract_names(); - auto xpress_solver = - absl::WrapUnique(new XpressSolver(std::move(xpr), extract_names)); + auto xpress_solver = absl::WrapUnique(new XpressSolver(std::move(xpr))); OR_RETURN_IF_ERROR(xpress_solver->LoadModel(model)); return xpress_solver; } @@ -990,17 +1022,15 @@ absl::Status XpressSolver::AddNewVariables( xpress_->AddVars(num_new_variables, {}, new_variables.lower_bounds(), new_variables.upper_bounds(), variable_type)); - if (extract_names_) { - OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_COLUMN, - num_old_variables, 0, num_new_variables, - new_variables)); - } + OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_COLUMN, + num_old_variables, 0, num_new_variables, + new_variables)); return absl::OkStatus(); } -XpressSolver::XpressSolver(std::unique_ptr g_xpress, bool extract_names) - : xpress_(std::move(g_xpress)), extract_names_(extract_names) {} +XpressSolver::XpressSolver(std::unique_ptr g_xpress) + : xpress_(std::move(g_xpress)) {} void XpressSolver::ExtractBounds(double lb, double ub, char& sense, double& rhs, double& rng) { @@ -1069,11 +1099,9 @@ absl::Status XpressSolver::AddNewLinearConstraints( // Add all constraints in one call. OR_RETURN_IF_ERROR( xpress_->AddConstrs(constraint_sense, constraint_rhs, constraint_rng)); - if (extract_names_) { - OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_ROW, - num_old_constraints, 0, num_new_constraints, - constraints)); - } + OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_ROW, + num_old_constraints, 0, num_new_constraints, + constraints)); return absl::OkStatus(); } @@ -1089,7 +1117,7 @@ absl::Status XpressSolver::AddObjective( // Moreover, in Xpress priorities are 32bit. // Note that ortools does not allow duplicate priorities, this is checked // by the caller. - if (objective.priority() <= INT_MIN || objective.priority() > INT_MAX) { + if (objective.priority() <= kint32min || objective.priority() > kint32max) { return ortools::InvalidArgumentErrorBuilder() << "Xpress only supports 32bit signed integers as objective " "priority, not " @@ -1101,7 +1129,7 @@ absl::Status XpressSolver::AddObjective( if (!multiobj) { // Not a multi-objective model OR_RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); - } else if (!objective_id.has_value()) { + } else if (!haveId) { // First objective in multi-objective. OR_RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); is_multiobj_ = true; @@ -1122,7 +1150,7 @@ absl::Status XpressSolver::AddObjective( // Quadratic terms const int num_terms = objective.quadratic_coefficients().row_ids().size(); if (num_terms > 0) { - if (multiobj && objective_id.has_value()) { + if (multiobj && haveId) { return ortools::InvalidArgumentErrorBuilder() << "Xpress does not support quadratic terms in anything but the " "first objective"; @@ -1153,7 +1181,7 @@ absl::Status XpressSolver::AddObjective( } if (multiobj) { - if (!objective_id.has_value()) { + if (!haveId) { // Primary objective OR_RETURN_IF_ERROR(xpress_->SetLinearObjective( objective.offset(), index, objective.linear_coefficients().values())); @@ -1172,7 +1200,7 @@ absl::Status XpressSolver::AddObjective( absl::MakeSpan(index), objective.linear_coefficients().values(), // checked above static_cast(-objective.priority()), weight)); - gtl::InsertOrDie(&objectives_map_, objective_id.value(), newid); + gtl::InsertOrDie(&objectives_map_, haveId, newid); } } else { OR_RETURN_IF_ERROR(xpress_->SetLinearObjective( @@ -1232,10 +1260,8 @@ absl::Status XpressSolver::AddSOS( } std::vector settype(start.size(), sos1 ? '1' : '2'); OR_RETURN_IF_ERROR(xpress_->AddSets(settype, start, colind, refval)); - if (extract_names_) { - OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_SET, num_old_sets, - sets.begin(), sets.end(), sets)); - } + OR_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_SET, num_old_sets, + sets.begin(), sets.end(), sets)); return absl::OkStatus(); } @@ -1306,8 +1332,8 @@ absl::Status XpressSolver::AddIndicators( std::vector i_complement(count); OR_ASSIGN_OR_RETURN(int const oldRows, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); - int min_icol = std::numeric_limits::max(); - int max_icol = std::numeric_limits::min(); + int min_icol = kint32max; + int max_icol = kint32min; bool check_types = false; int next = 0; for (auto const& [ortoolsId, indicator] : indicators) { @@ -1374,8 +1400,8 @@ absl::Status XpressSolver::AddIndicators( // Convert to binary if within range. if (orig_lb[idx] >= 0.0 && orig_lb[idx] <= 1.0 && orig_ub[idx] >= 0.0 && orig_ub[idx] <= 1.0) { - double const l = ceil(orig_lb[idx]); - double const u = floor(orig_ub[idx]); + double const l = std::ceil(orig_lb[idx]); + double const u = std::floor(orig_ub[idx]); orig_lb[idx] = l; // In case variable is indicator more than once orig_ub[idx] = u; // It would require less storage if we performed two calls to @@ -1405,10 +1431,10 @@ absl::Status XpressSolver::AddIndicators( // Change column type and bounds. Note that we must first change the type // since changing the type to 'B' will automatically change bounds to [0,1]. // After that we can fix up the bounds. - if (colind_type.size()) { + if (!colind_type.empty()) { OR_RETURN_IF_ERROR(xpress_->ChgColType(colind_type, new_type)); } - if (colind_bnd.size()) { + if (!colind_bnd.empty()) { OR_RETURN_IF_ERROR(xpress_->ChgBounds(colind_bnd, new_bdtype, new_bds)); } } @@ -1511,9 +1537,9 @@ absl::Status XpressSolver::ChangeCoefficients( absl::StatusOr XpressSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, - MessageCallback message_callback, - const CallbackRegistrationProto& callback_registration, Callback, - const SolveInterrupter* interrupter) { + SolverInterface::MessageCallback message_callback, + const CallbackRegistrationProto& callback_registration, + Callback /*callback*/, const SolveInterrupter* absl_nullable interrupter) { force_postsolve_ = false; primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; @@ -1556,12 +1582,12 @@ absl::StatusOr XpressSolver::Solve( // We are ready to solve the problem. If we are asked to export the // problem, then do that now. Depending on the file name extension we // either create a save file or an LP/MPS file. - if (export_model.length() > 0) { + if (!export_model.empty()) { if (export_model.length() >= 4 && export_model.compare(export_model.length() - 4, 4, ".svf") == 0) { - OR_RETURN_IF_ERROR(xpress_->SaveAs(export_model.c_str())); + OR_RETURN_IF_ERROR(xpress_->SaveAs(export_model)); } else { - OR_RETURN_IF_ERROR(xpress_->WriteProb(export_model.c_str())); + OR_RETURN_IF_ERROR(xpress_->WriteProb(export_model)); } } @@ -1571,12 +1597,12 @@ absl::StatusOr XpressSolver::Solve( // LPFLAGS. OR_RETURN_IF_ERROR( xpress_->Optimize(stop_after_lp_ ? "l" : "", &solvestatus_, &solstatus_)); - // Reraise any exception now. Note that we cannot just limit the scope of + // Return any callback error now. Note that we cannot just limit the scope of // solveContext since its destructor will restore controls settings. // On the other hand, when fetching results we need to check some controls // (for example, BARALG to decide whether we need to report barrier or // first order iterations). - solveContext.ReraiseException(); + OR_RETURN_IF_ERROR(solveContext.GetCallbackStatus()); OR_RETURN_IF_ERROR( xpress_->GetSolution(&primal_sol_avail_, std::nullopt, 0, -1)); OR_RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, std::nullopt, 0, -1)); @@ -1640,10 +1666,9 @@ absl::StatusOr XpressSolver::GetBestPrimalBound() const { absl::StatusOr XpressSolver::GetBestDualBound() const { if (is_mip_) { return xpress_->GetDoubleAttr(XPRS_BESTBOUND); - } - // Xpress does not have an attribute to report the best dual bound from - // simplex - else { + } else { + // Xpress does not have an attribute to report the best dual bound from + // simplex OR_ASSIGN_OR_RETURN(int const alg, xpress_->GetIntAttr(XPRS_ALGORITHM)); if (alg == XPRS_ALG_BARRIER) return xpress_->GetDoubleAttr(XPRS_BARDUALOBJ); @@ -1663,6 +1688,11 @@ absl::Status XpressSolver::ExtendWithMultiobj(SolutionProto& solution) { // We may not have solved for all objectives, so make sure we query only // those that were solved. OR_ASSIGN_OR_RETURN(int const nSolved, xpress_->GetIntAttr(XPRS_SOLVEDOBJS)); + if (nSolved == 0) return absl::OkStatus(); + if (nSolved < objectives_map_.size()) { + LOG(WARNING) << "Only " << nSolved << " objectives were solved, but " + << objectives_map_.size() << " were requested."; + } auto* objvals = solution.mutable_primal_solution()->mutable_auxiliary_objective_values(); for (auto const& [ortoolsId, xpressId] : objectives_map_) { @@ -1724,7 +1754,8 @@ absl::Status XpressSolver::AppendSolution( SolutionProto solution{}; bool storeSolutions = (solvestatus_ == XPRS_SOLVESTATUS_STOPPED || - solvestatus_ == XPRS_SOLVESTATUS_COMPLETED); + solvestatus_ == XPRS_SOLVESTATUS_COMPLETED) || + hasSolution; if (isPrimalFeasible()) { // The preferred methods for obtaining primal information are @@ -2121,14 +2152,15 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { // Not implemented yet // We can only update if problem is not in presolved state. - OR_RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; + // OR_RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; return false; } absl::StatusOr -XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, - MessageCallback message_callback, - const SolveInterrupter* interrupter) { +XpressSolver::ComputeInfeasibleSubsystem( + const SolveParametersProto& parameters, + SolverInterface::MessageCallback message_callback, + const SolveInterrupter* absl_nullable interrupter) { OR_RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ScopedSolverContext solveContext(xpress_.get()); OR_RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 4faf9840326..1069e888980 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include "absl/base/nullability.h" #include "absl/container/linked_hash_map.h" @@ -54,23 +54,20 @@ class XpressSolver : public SolverInterface { absl::StatusOr Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, - MessageCallback message_cb, - const CallbackRegistrationProto& callback_registration, Callback cb, - const SolveInterrupter* interrupter) override; + SolverInterface::MessageCallback message_callback, + const CallbackRegistrationProto& callback_registration, Callback callback, + const SolveInterrupter* absl_nullable interrupter) override; // Updates the problem (not implemented yet) absl::StatusOr Update(const ModelUpdateProto& model_update) override; // Computes the infeasible subsystem (not implemented yet) absl::StatusOr - ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, - MessageCallback message_cb, - const SolveInterrupter* interrupter) override; - - private: - explicit XpressSolver(std::unique_ptr g_xpress, bool extract_names); + ComputeInfeasibleSubsystem( + const SolveParametersProto& parameters, + SolverInterface::MessageCallback message_callback, + const SolveInterrupter* absl_nullable interrupter) override; - public: // For easing reading the code, we declare these types: using VarId = int64_t; using AuxiliaryObjectiveId = int64_t; @@ -89,7 +86,16 @@ class XpressSolver : public SolverInterface { using XpressGeneralConstraintIndex = int; using XpressAnyConstraintIndex = int; + // Data associated with each linear constraint + struct LinearConstraintData { + XpressLinearConstraintIndex constraint_index = kUnspecifiedConstraint; + double lower_bound = kMinusInf; + double upper_bound = kPlusInf; + }; + private: + explicit XpressSolver(std::unique_ptr g_xpress); + static constexpr XpressVariableIndex kUnspecifiedIndex = -1; static constexpr XpressAnyConstraintIndex kUnspecifiedConstraint = -2; static constexpr double kPlusInf = XPRS_PLUSINFINITY; @@ -99,15 +105,6 @@ class XpressSolver : public SolverInterface { return value < kPlusInf && value > kMinusInf; } - // Data associated with each linear constraint - public: - struct LinearConstraintData { - XpressLinearConstraintIndex constraint_index = kUnspecifiedConstraint; - double lower_bound = kMinusInf; - double upper_bound = kPlusInf; - }; - - private: absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); @@ -173,7 +170,6 @@ class XpressSolver : public SolverInterface { const SparseVectorFilterProto& filter) const; const std::unique_ptr xpress_; - bool const extract_names_; // Internal correspondence from variable proto IDs to Xpress-numbered // variables. diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index c0551eb81b5..67517f34769 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -37,6 +37,7 @@ #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" +#include "ortools/math_opt/solver_tests/test_models.h" #include "ortools/third_party_solvers/xpress_environment.h" /** A string in the log file that indicates that the solution process @@ -124,15 +125,13 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, testing::ValuesIn( - {CallbackTestParams(SolverType::kXpress, - /*integer_variables=*/false, + {CallbackTestParams(SolverType::kXpress, TestModelClass::kLp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, /*all_solutions=*/std::nullopt, /*reaches_cut_callback*/ std::nullopt), - CallbackTestParams(SolverType::kXpress, - /*integer_variables=*/true, + CallbackTestParams(SolverType::kXpress, TestModelClass::kIp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, @@ -141,17 +140,16 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( XpressInvalidInputTest, InvalidInputTest, - testing::ValuesIn( - {InvalidInputTestParameters(SolverType::kXpress, - /*use_integer_variables=*/true), - InvalidInputTestParameters(SolverType::kXpress, - /*use_integer_variables=*/false)})); + testing::ValuesIn({InvalidInputTestParameters(SolverType::kXpress, + TestModelClass::kIp), + InvalidInputTestParameters(SolverType::kXpress, + TestModelClass::kLp)})); InvalidParameterTestParams InvalidObjectiveLimitParameters() { SolveParameters params; params.objective_limit = 1.5; return InvalidParameterTestParams( - SolverType::kXpress, std::move(params), + SolverType::kXpress, TestModelClass::kLp, std::move(params), {"XpressSolver does not support objective_limit"}); } @@ -159,7 +157,7 @@ InvalidParameterTestParams InvalidBestBoundLimitParameters() { SolveParameters params; params.best_bound_limit = 1.5; return InvalidParameterTestParams( - SolverType::kXpress, std::move(params), + SolverType::kXpress, TestModelClass::kLp, std::move(params), {"XpressSolver does not support best_bound_limit"}); } @@ -167,7 +165,7 @@ InvalidParameterTestParams InvalidSolutionPoolSizeParameters() { SolveParameters params; params.solution_pool_size = 2; return InvalidParameterTestParams( - SolverType::kXpress, std::move(params), + SolverType::kXpress, TestModelClass::kLp, std::move(params), {"XpressSolver does not support solution_pool_size"}); } @@ -181,11 +179,11 @@ INSTANTIATE_TEST_SUITE_P( testing::ValuesIn( {GenericTestParameters(SolverType::kXpress, /*support_interrupter=*/true, - /*integer_variables=*/false, + TestModelClass::kLp, /*expected_log=*/OPTIMAL_SOLUTION_FOUND_LP), GenericTestParameters(SolverType::kXpress, /*support_interrupter=*/true, - /*integer_variables=*/true, + TestModelClass::kIp, /*expected_log=*/OPTIMAL_SOLUTION_FOUND_MIP)})); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); @@ -278,14 +276,13 @@ LogicalConstraintTestParameters GetXpressLogicalConstraintTestParameters() { // solely of variables (not expressions) and it does not support // duplicate entries. Many of the SOS tests construct things // like this, so we skip them. - /*supports_sos1=*/false, - /*supports_sos2=*/false, + /*supports_sos1=*/true, + /*supports_sos2=*/true, /*supports_indicator_constraints=*/true, /*supports_incremental_add_and_deletes=*/false, /*supports_incremental_variable_deletions=*/false, /*supports_deleting_indicator_variables=*/false, - /*supports_updating_binary_variables=*/false, - /*supports_sos_on_expressions=*/false); + /*supports_updating_binary_variables=*/false); } INSTANTIATE_TEST_SUITE_P( diff --git a/ortools/math_opt/testing/BUILD.bazel b/ortools/math_opt/testing/BUILD.bazel index ec32eaf17b0..a43af085282 100644 --- a/ortools/math_opt/testing/BUILD.bazel +++ b/ortools/math_opt/testing/BUILD.bazel @@ -12,8 +12,13 @@ # limitations under the License. load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = [ + "//experimental/users/rander/santorini:__subpackages__", + "//ortools/javatests/com/google/ortools/mathopt:__subpackages__", + "//visibility:public", +]) cc_library( name = "param_name", @@ -28,3 +33,21 @@ cc_library( name = "stream", hdrs = ["stream.h"], ) + +cc_test( + name = "param_name_test", + srcs = ["param_name_test.cc"], + deps = [ + ":param_name", + "//ortools/base:gmock_main", + ], +) + +cc_test( + name = "stream_test", + srcs = ["stream_test.cc"], + deps = [ + ":stream", + "//ortools/base:gmock_main", + ], +) diff --git a/ortools/math_opt/testing/param_name_test.cc b/ortools/math_opt/testing/param_name_test.cc new file mode 100644 index 00000000000..ee272773b4a --- /dev/null +++ b/ortools/math_opt/testing/param_name_test.cc @@ -0,0 +1,34 @@ +// 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/math_opt/testing/param_name.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research::math_opt { +namespace { + +struct DemoNamedParam { + std::string name; +}; + +TEST(PrintNamedParamTest, PrintName) { + const testing::TestParamInfo test_param({.name = "xyz"}, 0); + const ParamName printer; + EXPECT_EQ(printer(test_param), "xyz"); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/testing/stream_test.cc b/ortools/math_opt/testing/stream_test.cc new file mode 100644 index 00000000000..72b2b302ccc --- /dev/null +++ b/ortools/math_opt/testing/stream_test.cc @@ -0,0 +1,37 @@ +// 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/math_opt/testing/stream.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research::math_opt { +namespace { + +struct TestStruct { + int integer = 0; +}; + +std::ostream& operator<<(std::ostream& ostr, const TestStruct& value) { + ostr << "{integer: " << value.integer << "}"; + return ostr; +} + +TEST(StreamToStringTest, StreamToString) { + EXPECT_EQ(StreamToString(TestStruct{.integer = 1}), "{integer: 1}"); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index a1f76a6b8c6..79d3efc2928 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -13,7 +13,7 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//ortools/math_opt:__subpackages__"]) cc_library( name = "ids_validator", diff --git a/ortools/pdlp/BUILD.bazel b/ortools/pdlp/BUILD.bazel index 1f53d40fd1e..e289e8b0ad7 100644 --- a/ortools/pdlp/BUILD.bazel +++ b/ortools/pdlp/BUILD.bazel @@ -224,6 +224,7 @@ cc_library( hdrs = ["quadratic_program_io.h"], deps = [ ":quadratic_program", + "//ortools/base:bzip2file", "//ortools/base:file", "//ortools/base:gzipfile", "//ortools/base:mathutil", diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index fd1bbe9528a..7e685bc4e1f 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -49,7 +49,9 @@ cc_library( name = "filelineiter", hdrs = ["filelineiter.h"], deps = [ + "//ortools/base:bzip2file", "//ortools/base:file", + "//ortools/base:gzipfile", "@abseil-cpp//absl/log", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:string_view", @@ -223,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", @@ -362,6 +363,12 @@ cc_library( ], ) +proto_library( + name = "file_util_test_proto", + testonly = True, + srcs = ["file_util_test.proto"], +) + proto_library( name = "status_proto", srcs = ["status.proto"], @@ -443,9 +450,11 @@ cc_library( srcs = ["fp_roundtrip_conv.cc"], hdrs = ["fp_roundtrip_conv.h"], deps = [ + "//ortools/base:status_builder", "//ortools/base:status_macros", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", @@ -516,3 +525,45 @@ cc_library( hdrs = ["scheduling.h"], deps = ["@abseil-cpp//absl/log:check"], ) + +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_library( + name = "random_subset", + hdrs = ["random_subset.h"], + deps = [ + "//ortools/base:types", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/random:bit_gen_ref", + "@abseil-cpp//absl/random:distributions", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "data_path_resolver", + srcs = ["data_path_resolver.cc"], + hdrs = ["data_path_resolver.h"], + defines = ["ORTOOLS_USE_RUNFILES=1"], + deps = [ + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@rules_cc//cc/runfiles", + ], +) diff --git a/ortools/util/data_path_resolver.cc b/ortools/util/data_path_resolver.cc new file mode 100644 index 00000000000..798aa1ff917 --- /dev/null +++ b/ortools/util/data_path_resolver.cc @@ -0,0 +1,41 @@ +// 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/data_path_resolver.h" + +#include + +#include "absl/log/log.h" // IWYU pragma: keep +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#ifdef ORTOOLS_USE_RUNFILES +#include "rules_cc/cc/runfiles/runfiles.h" +#endif // ORTOOLS_USE_RUNFILES + +namespace ortools { + +std::string GetDataDependencyFilepath(absl::string_view relative_path) { +#ifdef ORTOOLS_USE_RUNFILES + using ::rules_cc::cc::runfiles::Runfiles; + std::string error; + std::unique_ptr runfiles(Runfiles::CreateForTest(&error)); + if (runfiles == nullptr) { + LOG(FATAL) << "Failed to create Runfiles: " << error; + } + return runfiles->Rlocation(absl::StrCat("_main/", relative_path)); +#else + return std::string(relative_path); +#endif // ORTOOLS_USE_RUNFILES +} + +} // namespace ortools diff --git a/ortools/util/data_path_resolver.h b/ortools/util/data_path_resolver.h new file mode 100644 index 00000000000..e5821ad7078 --- /dev/null +++ b/ortools/util/data_path_resolver.h @@ -0,0 +1,28 @@ +// 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_DATA_PATH_RESOLVER_H_ +#define ORTOOLS_UTIL_DATA_PATH_RESOLVER_H_ + +#include + +#include "absl/strings/string_view.h" + +namespace ortools { + +// Resolves a path relative to the root directory/runfiles. +std::string GetDataDependencyFilepath(absl::string_view relative_path); + +} // namespace ortools + +#endif // ORTOOLS_UTIL_DATA_PATH_RESOLVER_H_ diff --git a/ortools/util/file_util_test.proto b/ortools/util/file_util_test.proto new file mode 100644 index 00000000000..a91ea58d078 --- /dev/null +++ b/ortools/util/file_util_test.proto @@ -0,0 +1,25 @@ +// 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. + +edition = "2024"; + +package operations_research; + +message FileUtilTestProto1 { + double some_field = 1; +} + +message FileUtilTestProto2 { + double optional_field = 1; + repeated FileUtilTestProto1 repeated_fields = 2; +} diff --git a/ortools/util/filelineiter.h b/ortools/util/filelineiter.h index 427c7ed6b9b..e859734f09a 100644 --- a/ortools/util/filelineiter.h +++ b/ortools/util/filelineiter.h @@ -30,7 +30,16 @@ #include #include "absl/log/log.h" +#include "absl/strings/match.h" +#include "absl/strings/string_view.h" +#include "ortools/base/basictypes.h" +#include "ortools/base/bzip2file.h" #include "ortools/base/file.h" +#include "ortools/base/gzipfile.h" +#include "ortools/base/helpers.h" +#include "ortools/base/options.h" + +namespace operations_research { // Implements the minimum interface for a range-based for loop iterator. class FileLineIterator { @@ -135,6 +144,12 @@ class FileLines { if (!file_) { return; } + + if (absl::EndsWith(filename, ".bz2")) { + file_ = BZip2FileReader(filename, file_, TAKE_OWNERSHIP); + } else if (absl::EndsWith(filename, ".gz")) { + file_ = GZipFileReader(filename, file_, TAKE_OWNERSHIP); + } } // Initializes the FileLines ignoring errors. @@ -173,4 +188,5 @@ class FileLines { const int options_; }; +} // namespace operations_research #endif // ORTOOLS_UTIL_FILELINEITER_H_ diff --git a/ortools/util/logging.cc b/ortools/util/logging.cc index 669deff3920..8f248739d96 100644 --- a/ortools/util/logging.cc +++ b/ortools/util/logging.cc @@ -111,7 +111,7 @@ void SolverLogger::FlushPendingThrottledLogs(bool ignore_rates) { } PresolveTimer::~PresolveTimer() { - time_limit_->AdvanceDeterministicTime(work_); + time_limit_->AdvanceDeterministicTime(deterministic_time()); const double dtime = time_limit_->GetElapsedDeterministicTime() - dtime_at_start_; diff --git a/ortools/util/logging.h b/ortools/util/logging.h index 0884b390104..a771b5ca574 100644 --- a/ortools/util/logging.h +++ b/ortools/util/logging.h @@ -137,10 +137,10 @@ class PresolveTimer { // Track the work done (which is also the deterministic time). // By default we want a limit of around 1 deterministic seconds. void AddToWork(double dtime) { work_ += dtime; } - void TrackSimpleLoop(int size) { work_ += 5e-9 * size; } - void TrackHashLookups(int size) { work_ += 5e-8 * size; } - void TrackFastLoop(int size) { work_ += 1e-9 * size; } - bool WorkLimitIsReached() const { return work_ >= 1.0; } + void TrackSimpleLoop(int size) { work_int_ += 5 * size; } + void TrackHashLookups(int size) { work_int_ += 50 * size; } + void TrackFastLoop(int size) { work_int_ += size; } + bool WorkLimitIsReached() const { return deterministic_time() >= 1.0; } // Extra stats=value to display at the end. // We filter value of zero to have less clutter. @@ -162,7 +162,7 @@ class PresolveTimer { log_when_override_ = value; }; - double deterministic_time() const { return work_; } + double deterministic_time() const { return work_ + 1e-9 * work_int_; } double wtime() const { return timer_.Get(); } private: @@ -175,6 +175,7 @@ class PresolveTimer { bool override_logging_ = false; bool log_when_override_ = false; + int64_t work_int_ = 0; double work_ = 0.0; std::vector> counters_; std::vector extra_infos_; diff --git a/ortools/util/python/BUILD.bazel b/ortools/util/python/BUILD.bazel index 3e1557c35de..1d6509d6f64 100644 --- a/ortools/util/python/BUILD.bazel +++ b/ortools/util/python/BUILD.bazel @@ -183,7 +183,6 @@ cc_binary( srcs = ["gen_wrappers_test_message_pybind11.cc"], deps = [ ":wrappers", - "//ortools/base", "//ortools/util/testdata:wrappers_test_message_cc_proto", "@abseil-cpp//absl/flags:parse", "@abseil-cpp//absl/flags:usage", @@ -229,3 +228,19 @@ py_test( requirement("absl-py"), ], ) + +py_library( + name = "status_streaming", + srcs = ["status_streaming.py"], + deps = ["//ortools/util:status_py_pb2"], +) + +py_test( + name = "status_streaming_test", + srcs = ["status_streaming_test.py"], + deps = [ + ":status_streaming", + requirement("absl-py"), + "//ortools/util:status_py_pb2", + ], +) diff --git a/ortools/util/python/status_streaming.py b/ortools/util/python/status_streaming.py new file mode 100644 index 00000000000..70da89f1e1a --- /dev/null +++ b/ortools/util/python/status_streaming.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# 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. + +"""Translate ortools/util StatusProto to Python exceptions.""" + +import enum +import typing +from typing import Optional + +from ortools.util import status_pb2 + + +class StatusCode(enum.Enum): + """The C++ absl::Status::code() values.""" + + OK = 0 + CANCELLED = 1 + UNKNOWN = 2 + INVALID_ARGUMENT = 3 + DEADLINE_EXCEEDED = 4 + NOT_FOUND = 5 + ALREADY_EXISTS = 6 + PERMISSION_DENIED = 7 + UNAUTHENTICATED = 16 + RESOURCE_EXHAUSTED = 8 + FAILED_PRECONDITION = 9 + ABORTED = 10 + OUT_OF_RANGE = 11 + UNIMPLEMENTED = 12 + INTERNAL = 13 + UNAVAILABLE = 14 + DATA_LOSS = 15 + + +class StatusError(Exception): + """Base class for exception returned by status_proto_to_exception(). + + This class must not be instantiated; subclasses should. + + Some exceptions may inherit from another exception class to be caught as + regular Python errors. E.g., InvalidArgumentError also inherits from + ValueError, which would be the expected error thrown by an invalid argument in + Python. + + Attributes: + code: The code of the StatusProto (an integer). It should be one of the + known values in StatusCode, except for UnexpectedCodeError. + message: The message of the StatusProto. + """ + + def __init__(self, code: int, message: str): + try: + code_name = StatusCode(code).name + except ValueError: + code_name = str(code) + super().__init__(f"{message} (was C++ {code_name})") + self.code = code + self.message = message + + +class CancelledError(StatusError): + """Exception corresponding to StatusCode.CANCELLED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.CANCELLED.value, message) + + +class UnknownError(StatusError): + """Exception corresponding to StatusCode.UNKNOWN.""" + + def __init__(self, message: str): + super().__init__(StatusCode.UNKNOWN.value, message) + + +class InvalidArgumentError(StatusError, ValueError): + """Exception corresponding to StatusCode.INVALID_ARGUMENT.""" + + def __init__(self, message: str): + super().__init__(StatusCode.INVALID_ARGUMENT.value, message) + + +class DeadlineExceededError(StatusError): + """Exception corresponding to StatusCode.DEADLINE_EXCEEDED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.DEADLINE_EXCEEDED.value, message) + + +class NotFoundError(StatusError): + """Exception corresponding to StatusCode.NOT_FOUND.""" + + def __init__(self, message: str): + super().__init__(StatusCode.NOT_FOUND.value, message) + + +class AlreadyExistsError(StatusError): + """Exception corresponding to StatusCode.ALREADY_EXISTS.""" + + def __init__(self, message: str): + super().__init__(StatusCode.ALREADY_EXISTS.value, message) + + +class PermissionDeniedError(StatusError): + """Exception corresponding to StatusCode.PERMISSION_DENIED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.PERMISSION_DENIED.value, message) + + +class UnauthenticatedError(StatusError): + """Exception corresponding to StatusCode.UNAUTHENTICATED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.UNAUTHENTICATED.value, message) + + +class ResourceExhaustedError(StatusError): + """Exception corresponding to StatusCode.RESOURCE_EXHAUSTED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.RESOURCE_EXHAUSTED.value, message) + + +class FailedPreconditionError(StatusError, AssertionError): + """Exception corresponding to StatusCode.FAILED_PRECONDITION.""" + + def __init__(self, message: str): + super().__init__(StatusCode.FAILED_PRECONDITION.value, message) + + +class AbortedError(StatusError): + """Exception corresponding to StatusCode.ABORTED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.ABORTED.value, message) + + +class OutOfRangeError(StatusError): + """Exception corresponding to StatusCode.OUT_OF_RANGE.""" + + def __init__(self, message: str): + super().__init__(StatusCode.OUT_OF_RANGE.value, message) + + +class UnimplementedError(StatusError, NotImplementedError): + """Exception corresponding to StatusCode.UNIMPLEMENTED.""" + + def __init__(self, message: str): + super().__init__(StatusCode.UNIMPLEMENTED.value, message) + + +class InternalError(StatusError): + """Exception corresponding to StatusCode.INTERNAL.""" + + def __init__(self, message: str): + super().__init__(StatusCode.INTERNAL.value, message) + + +class UnavailableError(StatusError): + """Exception corresponding to StatusCode.UNAVAILABLE.""" + + def __init__(self, message: str): + super().__init__(StatusCode.UNAVAILABLE.value, message) + + +class DataLossError(StatusError): + """Exception corresponding to StatusCode.DATA_LOSS.""" + + def __init__(self, message: str): + super().__init__(StatusCode.DATA_LOSS.value, message) + + +class UnexpectedCodeError(StatusError): + """Exception corresponding to a code not listed in StatusCode. + + This is different from UnknownError which is the error for the + StatusCode.UNKNOWN (2). This error is used when StatusProto.code is not a + value in StatusCode (which should be rare). + """ + + +def status_proto_to_exception( + status_proto: status_pb2.StatusProto, +) -> Optional[StatusError]: + """Returns a Python exception that matches the input absl::Status. + + Some of StatusError sub-classes also inherit from another Python exception + that a Python library would use: + - InvalidArgumentError: ValueError + - FailedPreconditionError: AssertionError + - UnimplementedError: NotImplementedError + + Args: + status_proto: The input proto to convert to an exception. + + Returns: + The corresponding exception. None if the input status is OK. If the input + status_proto.code is not matching any value from StatusCode, an + UnexpectedCodeError is returned. + """ + try: + code = StatusCode(status_proto.code) + except ValueError: + return UnexpectedCodeError(status_proto.code, status_proto.message) + + # Define a short variable name to make all lines below fit in 80 columns. + msg = status_proto.message + + match code: + case StatusCode.OK: + return None + case StatusCode.CANCELLED: + return CancelledError(msg) + case StatusCode.UNKNOWN: + return UnknownError(msg) + case StatusCode.INVALID_ARGUMENT: + return InvalidArgumentError(msg) + case StatusCode.DEADLINE_EXCEEDED: + return DeadlineExceededError(msg) + case StatusCode.NOT_FOUND: + return NotFoundError(msg) + case StatusCode.ALREADY_EXISTS: + return AlreadyExistsError(msg) + case StatusCode.PERMISSION_DENIED: + return PermissionDeniedError(msg) + case StatusCode.UNAUTHENTICATED: + return UnauthenticatedError(msg) + case StatusCode.RESOURCE_EXHAUSTED: + return ResourceExhaustedError(msg) + case StatusCode.FAILED_PRECONDITION: + return FailedPreconditionError(msg) + case StatusCode.ABORTED: + return AbortedError(msg) + case StatusCode.OUT_OF_RANGE: + return OutOfRangeError(msg) + case StatusCode.UNIMPLEMENTED: + return UnimplementedError(msg) + case StatusCode.INTERNAL: + return InternalError(msg) + case StatusCode.UNAVAILABLE: + return UnavailableError(msg) + case StatusCode.DATA_LOSS: + return DataLossError(msg) + case _ as unreachable: + typing.assert_never(unreachable) diff --git a/ortools/util/python/status_streaming_test.py b/ortools/util/python/status_streaming_test.py new file mode 100644 index 00000000000..ae9686fc1e2 --- /dev/null +++ b/ortools/util/python/status_streaming_test.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# 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. + +"""Tests of the `status_streaming` package.""" + +from absl.testing import absltest + +from ortools.util import status_pb2 +from ortools.util.python import status_streaming + + +class StatusProtoToExceptionTest(absltest.TestCase): + + def test_ok(self) -> None: + self.assertIsNone( + status_streaming.status_proto_to_exception( + status_pb2.StatusProto(code=status_streaming.StatusCode.OK.value) + ) + ) + + def test_invalid_argument(self) -> None: + error = status_streaming.status_proto_to_exception( + status_pb2.StatusProto( + code=status_streaming.StatusCode.INVALID_ARGUMENT.value, + message="something", + ) + ) + self.assertIsInstance(error, status_streaming.InvalidArgumentError) + self.assertIsInstance(error, ValueError) + self.assertEqual(str(error), "something (was C++ INVALID_ARGUMENT)") + + def test_failed_precondition(self) -> None: + error = status_streaming.status_proto_to_exception( + status_pb2.StatusProto( + code=status_streaming.StatusCode.FAILED_PRECONDITION.value, + message="something", + ) + ) + self.assertIsInstance(error, status_streaming.FailedPreconditionError) + self.assertIsInstance(error, AssertionError) + self.assertEqual(str(error), "something (was C++ FAILED_PRECONDITION)") + + def test_unimplemented(self) -> None: + error = status_streaming.status_proto_to_exception( + status_pb2.StatusProto( + code=status_streaming.StatusCode.UNIMPLEMENTED.value, + message="something", + ) + ) + self.assertIsInstance(error, status_streaming.UnimplementedError) + self.assertIsInstance(error, NotImplementedError) + self.assertEqual(str(error), "something (was C++ UNIMPLEMENTED)") + + def test_all_error_codes(self) -> None: + """Tests all possible values in StatusCode but OK.""" + for code in status_streaming.StatusCode: + if code == status_streaming.StatusCode.OK: + continue + with self.subTest(code=code): + error = status_streaming.status_proto_to_exception( + status_pb2.StatusProto(code=code.value, message="something") + ) + + # Errors must be instances of sub-classes of StatusError; not an + # instance of this base class. + self.assertIsNot(type(error), status_streaming.StatusError) + self.assertIsInstance(error, status_streaming.StatusError) + + # Test that the exception class name follows the expected pattern. + camel_case_code = "".join( + word.capitalize() for word in code.name.lower().split("_") + ) + self.assertEqual(type(error).__name__, f"{camel_case_code}Error") + + # Test that the message is not lost and that the C++ code is included. + self.assertEqual(str(error), f"something (was C++ {code.name})") + + def test_unknown_code(self) -> None: + error = status_streaming.status_proto_to_exception( + status_pb2.StatusProto(code=-5, message="something") + ) + self.assertIsInstance(error, status_streaming.UnexpectedCodeError) + self.assertEqual(str(error), "something (was C++ -5)") + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/util/status_streaming.cc b/ortools/util/status_streaming.cc new file mode 100644 index 00000000000..f6794958973 --- /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 ortools::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, + ortools::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..562b1281c5b --- /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, + ortools::SourceLocation warning_loc = ortools::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, + ortools::SourceLocation loc = ortools::SourceLocation::current()); + +} // namespace operations_research + +#endif // ORTOOLS_UTIL_STATUS_STREAMING_H_