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:
+ *
+ *
+ * - 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.
+ *
+ */
+ 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