Repository: dgasmith/opt_einsum
Pull request #61 (2018-10-01 00:50:50)

various changes to path machinery
## Description
In the process of comparing/benchmarking the new 'cheap' path (#60) I thought various consistency/convenience updates to the path machinery might be worthwhile. Some of these are bigger than others so definitely let me know what you think. 

1. Per #55, change the ``memory_limit`` default to unlimited. As I mentioned in the other thread, I think any time that the arrays are going to get too big for RAM, resorting to einsum is going to be so exponentially slow that a ``MemoryError`` might be preferable. We could add ``'warn'/'system'`` versions that take system memory into account, the only thing is that this requires checking the output dtype and getting the systems free memory which is a bit of overhead. This change does make 'optimal' a bit slower, but at the same time, it possibly wasn't obvious previously that the path found might not have been globally optimal. Happy to split/delay this into a later PR if necessary (@dgasmith?).

2. Deprecated (with a warning) the ``path=`` keyword argument in ``contract_path`` in favour of ``optimize=``. Other functions, including in ``numpy`` use ``optimize`` so I think this makes sense to have a matching signature between ``contract, contract_path``, & ``contract_expression``.

3. Factor the full path information into a class (``PathInfo``) and update it to allow printing flop costs ``>1e307`` (which previously errored). This allows access to the various path costs programatically (e.g. ``path.opt_cost``, ``path.largest_intermediate`` etc) and makes the default repr the full printed info so no need to ``print(path)``. This is a breaking change if anyone was using the ``path`` as a proper string (e.g. @fritzo I saw you were regexing this) but ultimately should negate the need to do that (and a cheap fix is just ``str(path)``).

4. Add helper function ``oe.helpers.rand_equation``. This is a just a private function that can be helpful to generate large random expressions with variable connectivity and is thus might be useful for testing.

5. Finally, the ``cupy`` tests weren't working - I think it needs to be imported explicitly, and before other GPU libs, in order to initialize CUDA properly, so I've done that.

## TODO:
- [x] Note the ``memory_limit`` choice in the docs somewhere?
- [x] Test the ``rand_equation`` generator / update some tests to use it?

## Status
- [x] Ready to go

codecov-io: # [Codecov](https://codecov.io/gh/dgasmith/opt_einsum/pull/61?src=pr&el=h1) Report
> Merging [#61](https://codecov.io/gh/dgasmith/opt_einsum/pull/61?src=pr&el=desc) into [master](https://codecov.io/gh/dgasmith/opt_einsum/commit/1db5a0ab3dd8003919d0fa38b455cc7bad1b4e80?src=pr&el=desc) will **decrease** coverage by `2.61%`.
> The diff coverage is `64.1%`.


jcmgray: @fritzo, great, I've made those couple of changes - thanks for pointing them out.
dgasmith: It might be good to write a few tests that use the random path tech with a static seed and compares that the results are the same across all paths.
jcmgray: Great I'll merge.

Patch:
diff --git a/docs/source/greedy_path.rst b/docs/source/greedy_path.rst
index 4a32ab7..d8301e4 100644
--- a/docs/source/greedy_path.rst
+++ b/docs/source/greedy_path.rst
@@ -25,7 +25,7 @@ It should be stressed these cases are quite rare and by default ``contract`` use
     >>> B = np.random.rand(37, 51, 51, 59)
     >>> C = np.random.rand(59, 27)
 
-    >>> path, desc = oe.contract_path('xyf,xtf,ytpf,fr->tpr', M, A, B, C, path="greedy")
+    >>> path, desc = oe.contract_path('xyf,xtf,ytpf,fr->tpr', M, A, B, C, optimize="greedy")
     >>> print(desc)
       Complete contraction:  xyf,xtf,ytpf,fr->tpr
              Naive scaling:  6
@@ -41,7 +41,7 @@ It should be stressed these cases are quite rare and by default ``contract`` use
        4          False          tpfx,xtf->tpf                           fr,tpf->tpr
        4           GEMM            tpf,fr->tpr                              tpr->tpr
 
-    >>> path, desc = oe.contract_path('xyf,xtf,ytpf,fr->tpr', M, A, B, C, path="optimal")
+    >>> path, desc = oe.contract_path('xyf,xtf,ytpf,fr->tpr', M, A, B, C, optimize="optimal")
     >>> print(desc)
 
       Complete contraction:  xyf,xtf,ytpf,fr->tpr
@@ -56,4 +56,4 @@ It should be stressed these cases are quite rare and by default ``contract`` use
     --------------------------------------------------------------------------------
        4          False           xtf,xyf->tfy                      ytpf,fr,tfy->tpr
        4          False          tfy,ytpf->tfp                           fr,tfp->tpr
-       4           TDOT            tfp,fr->tpr                              tpr->tpr
\ No newline at end of file
+       4           TDOT            tfp,fr->tpr                              tpr->tpr
diff --git a/opt_einsum/contract.py b/opt_einsum/contract.py
index 4d2f380..7e5e233 100644
--- a/opt_einsum/contract.py
+++ b/opt_einsum/contract.py
@@ -15,6 +15,70 @@ from . import sharing
 __all__ = ["contract_path", "contract", "format_const_einsum_str", "ContractExpression", "shape_only", "shape_only"]
 
 
+class PathInfo(object):
+
+    def __init__(self, contraction_list, input_subscripts, output_subscript,
+                 indices, scale_list, naive_cost, opt_cost, size_list):
+        self.contraction_list = contraction_list
+        self.input_subscripts = input_subscripts
+        self.output_subscript = output_subscript
+        self.indices = indices
+        self.scale_list = scale_list
+        self.naive_cost = naive_cost
+        self.opt_cost = opt_cost
+        self.largest_intermediate = max(size_list)
+
+    def __repr__(self):
+        from decimal import Decimal
+
+        # naive costs / speedups can easily reach >~ 1e308, need exact arithmetic
+        naive_cost = Decimal(self.naive_cost)
+        opt_cost = Decimal(self.opt_cost)
+        speedup = naive_cost / opt_cost
+        largest_intermediate = Decimal(self.largest_intermediate)
+
+        # Return the path along with a nice string representation
+        overall_contraction = self.input_subscripts + "->" + self.output_subscript
+        header = ("scaling", "BLAS", "current", "remaining")
+
+        path_print = [
+            "  Complete contraction:  {}\n".format(overall_contraction),
+            "         Naive scaling:  {}\n".format(len(self.indices)),
+            "     Optimized scaling:  {}\n".format(max(self.scale_list)),
+            "      Naive FLOP count:  {:.3e}\n".format(naive_cost),
+            "  Optimized FLOP count:  {:.3e}\n".format(opt_cost),
+            "   Theoretical speedup:  {:3.3f}\n".format(speedup),
+            "  Largest intermediate:  {:.3e} elements\n".format(largest_intermediate),
+            "-" * 80 + "\n",
+            "{:>6} {:>11} {:>22} {:>37}\n".format(*header),
+            "-" * 80
+        ]
+
+        for n, contraction in enumerate(self.contraction_list):
+            inds, idx_rm, einsum_str, remaining, do_blas = contraction
+            remaining_str = ",".join(remaining) + "->" + self.output_subscript
+            path_run = (self.scale_list[n], do_blas, einsum_str, remaining_str)
+            path_print.append("\n{:>4} {:>14} {:>22} {:>37}".format(*path_run))
+
+        return "".join(path_print)
+
+
+def _choose_memory_arg(memory_limit, size_list):
+    if memory_limit == 'max_input':
+        return max(size_list)
+
+    if memory_limit is None:
+        return int(1e20)
+
+    if memory_limit < 1:
+        if memory_limit == -1:
+            return int(1e20)
+        else:
+            raise ValueError("Memory limit must be larger than 0, or -1")
+
+    return int(memory_limit)
+
+
 def contract_path(*operands, **kwargs):
     """
     Evaluates the lowest cost einsum-like contraction order.
@@ -25,7 +89,7 @@ def contract_path(*operands, **kwargs):
         Specifies the subscripts for summation.
     *operands : list of array_like
         These are the arrays for the operation.
-    path : bool or list, optional (default: ``auto``)
+    optimize : bool or list, optional (default: ``auto``)
         Choose the type of path.
 
         - if a list is given uses this as the path.
@@ -111,12 +175,18 @@ def contract_path(*operands, **kwargs):
     """
 
     # Make sure all keywords are valid
-    valid_contract_kwargs = ['path', 'memory_limit', 'einsum_call', 'use_blas']
+    valid_contract_kwargs = ['optimize', 'path', 'memory_limit', 'einsum_call', 'use_blas']
     unknown_kwargs = [k for (k, v) in kwargs.items() if k not in valid_contract_kwargs]
     if len(unknown_kwargs):
-        raise TypeError("einsum_path: Did not understand the following kwargs: %s" % unknown_kwargs)
+        raise TypeError("einsum_path: Did not understand the following kwargs: {}".format(unknown_kwargs))
+
+    if 'path' in kwargs:
+        import warnings
+        warnings.warn("The 'path' keyword argument is deprecated in favor of 'optimize'.", DeprecationWarning)
+        path_type = kwargs.pop('path')
+    else:
+        path_type = kwargs.pop('optimize', 'auto')
 
-    path_type = kwargs.pop('path', 'auto')
     memory_limit = kwargs.pop('memory_limit', None)
 
     # Hidden option, only einsum should call this
@@ -139,8 +209,8 @@ def contract_path(*operands, **kwargs):
         sh = input_shps[tnum]
 
         if len(sh) != len(term):
-            raise ValueError("Einstein sum subscript %s does not contain the "
-                             "correct number of indices for operand %d." % (input_subscripts[tnum], tnum))
+            raise ValueError("Einstein sum subscript '{}' does not contain the "
+                             "correct number of indices for operand {}.".format(input_subscripts[tnum], tnum))
         for cnum, char in enumerate(term):
             dim = sh[cnum]
 
@@ -149,25 +219,14 @@ def contract_path(*operands, **kwargs):
                 if dimension_dict[char] == 1:
                     dimension_dict[char] = dim
                 elif dim not in (1, dimension_dict[char]):
-                    raise ValueError("Size of label '%s' for operand %d (%d) "
-                                     "does not match previous terms (%d)." % (char, tnum, dimension_dict[char], dim))
+                    raise ValueError("Size of label '{}' for operand {} ({}) does not match previous "
+                                     "terms ({}).".format(char, tnum, dimension_dict[char], dim))
             else:
                 dimension_dict[char] = dim
 
     # Compute size of each input array plus the output array
     size_list = [helpers.compute_size_by_dict(term, dimension_dict) for term in input_list + [output_subscript]]
-    out_size = max(size_list)
-
-    if memory_limit is None:
-        memory_arg = out_size
-    else:
-        if memory_limit < 1:
-            if memory_limit == -1:
-                memory_arg = int(1e20)
-            else:
-                raise ValueError("Memory limit must be larger than 0, or -1")
-        else:
-            memory_arg = int(memory_limit)
+    memory_arg = _choose_memory_arg(memory_limit, size_list)
 
     num_ops = len(input_list)
 
@@ -195,7 +254,7 @@ def contract_path(*operands, **kwargs):
     elif path_type in ("greedy", "opportunistic", "auto"):
         path = paths.greedy(input_sets, output_set, dimension_dict, memory_arg)
     else:
-        raise KeyError("Path name %s not found" % path_type)
+        raise KeyError("Path name '{}' not found".format(path_type))
 
     cost_list = []
     scale_list = []
@@ -247,26 +306,8 @@ def contract_path(*operands, **kwargs):
     if einsum_call_arg:
         return operands, contraction_list
 
-    # Return the path along with a nice string representation
-    overall_contraction = input_subscripts + "->" + output_subscript
-    header = ("scaling", "BLAS", "current", "remaining")
-
-    path_print = "  Complete contraction:  %s\n" % overall_contraction
-    path_print += "         Naive scaling:  %d\n" % len(indices)
-    path_print += "     Optimized scaling:  %d\n" % max(scale_list)
-    path_print += "      Naive FLOP count:  %.3e\n" % naive_cost
-    path_print += "  Optimized FLOP count:  %.3e\n" % opt_cost
-    path_print += "   Theoretical speedup:  %3.3f\n" % (naive_cost / float(opt_cost))
-    path_print += "  Largest intermediate:  %.3e elements\n" % max(size_list)
-    path_print += "-" * 80 + "\n"
-    path_print += "%6s %11s %22s %37s\n" % header
-    path_print += "-" * 80
-
-    for n, contraction in enumerate(contraction_list):
-        inds, idx_rm, einsum_str, remaining, do_blas = contraction
-        remaining_str = ",".join(remaining) + "->" + output_subscript
-        path_run = (scale_list[n], do_blas, einsum_str, remaining_str)
-        path_print += "\n%4d %14s %22s %37s" % path_run
+    path_print = PathInfo(contraction_list, input_subscripts, output_subscript,
+                          indices, scale_list, naive_cost, opt_cost, size_list)
 
     return path, path_print
 
@@ -350,11 +391,15 @@ def contract(*operands, **kwargs):
           contracting the listed tensors. Scales exponentially with
           the number of terms in the contraction.
 
-    memory_limit : int or None (default : None)
-        The upper limit of the size of tensor created, by default, this will be
+    memory_limit : {None, int, 'max_input'} (default: None)
         Give the upper bound of the largest intermediate tensor contract will build.
-        By default (None) will size the ``memory_limit`` as the largest input tensor.
-        Users can also specify ``-1`` to allow arbitrarily large tensors to be built.
+
+        - None or -1 means there is no limit
+        - 'max_input' means the limit is set as largest input tensor
+        - a positive integer is taken as an explicit limit on the number of elements
+
+        The default is None. Note that imposing a limit can make contractions
+        exponentially slower to perform.
     backend : str, optional (default: ``numpy``)
         Which library to use to perform the required ``tensordot``, ``transpose``
         and ``einsum`` calls. Should match the types of arrays supplied, See
@@ -407,14 +452,14 @@ def contract(*operands, **kwargs):
     # Make sure remaining keywords are valid for einsum
     unknown_kwargs = [k for (k, v) in kwargs.items() if k not in valid_einsum_kwargs]
     if len(unknown_kwargs):
-        raise TypeError("Did not understand the following kwargs: %s" % unknown_kwargs)
+        raise TypeError("Did not understand the following kwargs: {}".format(unknown_kwargs))
 
     if gen_expression:
         full_str = operands[0]
 
     # Build the contraction list and operand
     operands, contraction_list = contract_path(
-        *operands, path=optimize_arg, memory_limit=memory_limit, einsum_call=True, use_blas=use_blas)
+        *operands, optimize=optimize_arg, memory_limit=memory_limit, einsum_call=True, use_blas=use_blas)
 
     # check if performing contraction or just building expression
     if gen_expression:
@@ -630,13 +675,13 @@ class ContractExpression:
 
         if kwargs:
             raise ValueError("The only valid keyword arguments to a `ContractExpression` "
-                             "call are `out=` or `backend=`. Got: %s." % kwargs)
+                             "call are `out=` or `backend=`. Got: {}.".format(kwargs))
 
         correct_num_args = self._full_num_args if evaluate_constants else self.num_args
 
         if len(arrays) != correct_num_args:
-            raise ValueError("This `ContractExpression` takes exactly %s array arguments "
-                             "but received %s." % (self.num_args, len(arrays)))
+            raise ValueError("This `ContractExpression` takes exactly {} array arguments "
+                             "but received {}.".format(self.num_args, len(arrays)))
 
         if self._constants_dict and not evaluate_constants:
             # fill in the missing non-constant terms with newly supplied arrays
@@ -657,7 +702,7 @@ class ContractExpression:
             original_msg = str(err.args) if err.args else ""
             msg = ("Internal error while evaluating `ContractExpression`. Note that few checks are performed"
                    " - the number and rank of the array arguments must match the original expression. "
-                   "The internal error was: '%s'" % original_msg, )
+                   "The internal error was: '{}'".format(original_msg), )
             err.args = msg
             raise
 
@@ -669,13 +714,13 @@ class ContractExpression:
         return "<ContractExpression('{}'{})>".format(self.contraction, constants_repr)
 
     def __str__(self):
-        s = self.__repr__()
+        s = [self.__repr__()]
         for i, c in enumerate(self.contraction_list):
-            s += "\n  %i.  " % (i + 1)
-            s += "'%s'" % c[2] + (" [%s]" % c[-1] if c[-1] else "")
+            s.append("\n  {}.  ".format(i + 1))
+            s.append("'{}'".format(c[2]) + (" [{}]".format(c[-1]) if c[-1] else ""))
         if self.einsum_kwargs:
-            s += "\neinsum_kwargs=%s" % self.einsum_kwargs
-        return s
+            s.append("\neinsum_kwargs={}".format(self.einsum_kwargs))
+        return "".join(s)
 
 
 def shape_only(shape):
@@ -751,8 +796,8 @@ def contract_expression(subscripts, *shapes, **kwargs):
 
     for arg in ('out', 'backend'):
         if kwargs.get(arg, None) is not None:
-            raise ValueError("'%s' should only be specified when calling a "
-                             "`ContractExpression`, not when building it." % arg)
+            raise ValueError("'{}' should only be specified when calling a "
+                             "`ContractExpression`, not when building it.".format(arg))
 
     kwargs['_gen_expression'] = True
 
diff --git a/opt_einsum/helpers.py b/opt_einsum/helpers.py
index 9efe5ea..20dc64f 100644
--- a/opt_einsum/helpers.py
+++ b/opt_einsum/helpers.py
@@ -3,6 +3,7 @@ Contains helper functions for opt_einsum testing scripts
 """
 
 import numpy as np
+from .parser import get_symbol
 
 __all__ = ["build_views", "compute_size_by_dict", "find_contraction", "flop_count"]
 
@@ -167,3 +168,97 @@ def flop_count(idx_contraction, inner, num_terms, size_dictionary):
         op_factor += 1
 
     return overall_size * op_factor
+
+
+def rand_equation(n, reg, n_out=0, d_min=2, d_max=9, seed=None):
+    """Generate a random contraction and shapes.
+
+    Parameters
+    ----------
+    n : int
+        Number of array arguments.
+    reg : int
+        'Regularity' of the contraction graph. This essentially determines how
+        many indices each tensor shares with others on average.
+    n_out : int, optional
+        Number of output indices (i.e. the number of non-contracted indices).
+        Defaults to 0, i.e., a contraction resulting in a scalar.
+    d_min : int, optional
+        Minimum dimension size.
+    d_max : int, optional
+        Maximum dimension size.
+    seed: int, optional
+        If not None, seed numpy's random generator with this.
+
+    Returns
+    -------
+    eq : str
+        The equation string.
+    shapes : list[tuple[int]]
+        The array shapes.
+
+    Examples
+    --------
+    >>> eq, shapes = rand_equation(n=10, reg=4, n_out=5, seed=42)
+    >>> eq
+    'oyeqn,tmaq,skpo,vg,hxui,n,fwxmr,hitplcj,kudlgfv,rywjsb->cebda'
+
+    >>> shapes
+    [(9, 5, 4, 5, 4),
+     (4, 4, 8, 5),
+     (9, 4, 6, 9),
+     (6, 6),
+     (6, 9, 7, 8),
+     (4,),
+     (9, 3, 9, 4, 9),
+     (6, 8, 4, 6, 8, 6, 3),
+     (4, 7, 8, 8, 6, 9, 6),
+     (9, 5, 3, 3, 9, 5)]
+    """
+
+    if seed is not None:
+        np.random.seed(seed)
+
+    # total number of indices
+    num_inds = n * reg // 2 + n_out
+    inputs = ["" for _ in range(n)]
+    output = []
+
+    ix_szs = {
+        get_symbol(i): np.random.randint(d_min, d_max + 1)
+        for i in range(num_inds)
+    }
+
+    # generate a list of indices to place either once or twice
+    def gen():
+        for i, ix in enumerate(ix_szs):
+            # generate an outer index
+            if i < n_out:
+                output.append(ix)
+                yield ix
+            # generate a bond
+            else:
+                yield ix
+                yield ix
+
+    # add the indices randomly to the inputs
+    for i, ix in enumerate(np.random.permutation(list(gen()))):
+        # make sure all inputs have at least one index
+        if i < n:
+            inputs[i] += ix
+        else:
+            # don't add any traces on same op
+            where = np.random.randint(0, n)
+            while ix in inputs[where]:
+                where = np.random.randint(0, n)
+
+            inputs[where] += ix
+
+    # randomly transpose the output indices and form equation
+    output = "".join(np.random.permutation(output))
+    eq = "{}->{}".format(",".join(inputs), output)
+
+    # make the shapes
+    shapes = [tuple(ix_szs[ix] for ix in op) for op in inputs]
+
+    return eq, shapes
diff --git a/opt_einsum/parser.py b/opt_einsum/parser.py
index 2e9330e..910a977 100644
--- a/opt_einsum/parser.py
+++ b/opt_einsum/parser.py
@@ -321,7 +321,7 @@ def parse_einsum_input(operands):
     # Make sure output subscripts are in the input
     for char in output_subscript:
         if char not in input_subscripts:
-            raise ValueError("Output character %s did not appear in the input" % char)
+            raise ValueError("Output character '{}' did not appear in the input".format(char))
 
     # Make sure number operands is equivalent to the number of terms
     if len(input_subscripts.split(',')) != len(operands):
diff --git a/scripts/compare_random_paths.py b/scripts/compare_random_paths.py
index 3ece785..c594014 100644
--- a/scripts/compare_random_paths.py
+++ b/scripts/compare_random_paths.py
@@ -44,7 +44,7 @@ def get_string(term):
 
 def random_contraction():
 
-    # Compute number of terms    
+    # Compute number of terms
     num_terms = np.random.randint(min_terms, max_terms)
 
     # Compute size of each index
@@ -53,7 +53,7 @@ def random_contraction():
     # Build random terms and views
     int_terms = [make_term() for x in range(num_terms)]
     views = [np.random.rand(*index_size[s]) for s in int_terms]
-    
+
     # Compute einsum string and return string
     sum_string = ','.join([get_string(s) for s in int_terms])
     out_string = sum_string.replace(',','')
@@ -79,7 +79,7 @@ for x in range(200):
         out.append(['Opt_einsum failed', sum_string, index_size, 0, 0])
         continue
 
-    current_opt_path = oe.contract_path(sum_string, *views, path=opt_path)[0]
+    current_opt_path = oe.contract_path(sum_string, *views, optimize=opt_path)[0]
     if not np.allclose(ein, opt):
         out.append(['Comparison failed', sum_string, index_size, 0, 0])
         continue


Test patch:
diff --git a/opt_einsum/tests/test_backends.py b/opt_einsum/tests/test_backends.py
index d4ef06c..5506104 100644
--- a/opt_einsum/tests/test_backends.py
+++ b/opt_einsum/tests/test_backends.py
@@ -3,6 +3,12 @@ import pytest
 
 from opt_einsum import contract, helpers, contract_expression, backends, sharing
 
+try:
+    import cupy
+    found_cupy = True
+except ImportError:
+    found_cupy = False
+
 try:
     import tensorflow as tf
     # needed so tensorflow doesn't allocate all gpu mem
@@ -182,10 +188,9 @@ def test_theano_with_sharing(string):
     assert np.allclose(ein, thn2)
 
 
+@pytest.mark.skipif(not found_cupy, reason="Cupy not installed.")
 @pytest.mark.parametrize("string", tests)
 def test_cupy(string):  # pragma: no cover
-    cupy = pytest.importorskip("cupy")
-
     views = helpers.build_views(string)
     ein = contract(string, *views, optimize=False, use_blas=False)
     shps = [v.shape for v in views]
@@ -202,8 +207,8 @@ def test_cupy(string):  # pragma: no cover
     assert np.allclose(ein, cupy.asnumpy(cupy_opt))
 
 
+@pytest.mark.skipif(not found_cupy, reason="Cupy not installed.")
 def test_cupy_with_constants():  # pragma: no cover
-    cupy = pytest.importorskip("cupy")
 
     eq = 'ij,jk,kl->li'
     shapes = (2, 3), (3, 4), (4, 5)
diff --git a/opt_einsum/tests/test_contract.py b/opt_einsum/tests/test_contract.py
index 64fb275..22900c8 100644
--- a/opt_einsum/tests/test_contract.py
+++ b/opt_einsum/tests/test_contract.py
@@ -2,8 +2,6 @@
 Tets a series of opt_einsum contraction paths to ensure the results are the same for different paths
 """
 
-import sys
-
 import numpy as np
 import pytest
 
@@ -175,7 +173,7 @@ def test_printing():
     views = helpers.build_views(string)
 
     ein = contract_path(string, *views)
-    assert len(ein[1]) == 729
+    assert len(str(ein[1])) == 726
 
 
 @pytest.mark.parametrize("string", tests)
@@ -231,3 +229,17 @@ def test_contract_expression_with_constants(string, constants):
     print(expr)
     out = expr(*ctrc_args)
     assert np.allclose(expected, out)
+
+
+@pytest.mark.parametrize("optimize", ['greedy', 'optimal'])
+@pytest.mark.parametrize("n", [3, 5])
+@pytest.mark.parametrize("reg", [3, 4])
+@pytest.mark.parametrize("n_out", [0, 2, 4])
+def test_rand_equation(optimize, n, reg, n_out):
+    eq, shapes = helpers.rand_equation(n, reg, n_out, d_min=2, d_max=5, seed=42)
+    views = [np.random.rand(*s) for s in shapes]
+
+    expected = contract(eq, *views, optimize=False)
+    actual = contract(eq, *views, optimize=optimize)
+
+    assert np.allclose(expected, actual)
diff --git a/opt_einsum/tests/test_paths.py b/opt_einsum/tests/test_paths.py
index da39e23..dc61645 100644
--- a/opt_einsum/tests/test_paths.py
+++ b/opt_einsum/tests/test_paths.py
@@ -124,17 +124,17 @@ def test_memory_paths():
     views = oe.helpers.build_views(expression)
 
     # Test tiny memory limit
-    path_ret = oe.contract_path(expression, *views, path="optimal", memory_limit=5)
+    path_ret = oe.contract_path(expression, *views, optimize="optimal", memory_limit=5)
     assert check_path(path_ret[0], [(0, 1, 2, 3, 4, 5)])
 
-    path_ret = oe.contract_path(expression, *views, path="greedy", memory_limit=5)
+    path_ret = oe.contract_path(expression, *views, optimize="greedy", memory_limit=5)
     assert check_path(path_ret[0], [(0, 1, 2, 3, 4, 5)])
 
     # Check the possibilities, greedy is capped
-    path_ret = oe.contract_path(expression, *views, path="optimal", memory_limit=-1)
+    path_ret = oe.contract_path(expression, *views, optimize="optimal", memory_limit=-1)
     assert check_path(path_ret[0], [(0, 3), (0, 4), (0, 2), (0, 2), (0, 1)])
 
-    path_ret = oe.contract_path(expression, *views, path="greedy", memory_limit=-1)
+    path_ret = oe.contract_path(expression, *views, optimize="greedy", memory_limit=-1)
     assert check_path(path_ret[0], [(0, 3), (0, 4), (0, 2), (0, 2), (0, 1)])
 
 
@@ -143,7 +143,7 @@ def test_path_edge_cases(alg, expression, order):
     views = oe.helpers.build_views(expression)
 
     # Test tiny memory limit
-    path_ret = oe.contract_path(expression, *views, path=alg)
+    path_ret = oe.contract_path(expression, *views, optimize=alg)
     assert check_path(path_ret[0], order)
 
 
@@ -152,10 +152,10 @@ def test_optimal_edge_cases():
     # Edge test5
     expression = 'a,ac,ab,ad,cd,bd,bc->'
     edge_test4 = oe.helpers.build_views(expression, dimension_dict={"a": 20, "b": 20, "c": 20, "d": 20})
-    path, path_str = oe.contract_path(expression, *edge_test4, path='greedy')
+    path, path_str = oe.contract_path(expression, *edge_test4, optimize='greedy', memory_limit='max_input')
     assert check_path(path, [(0, 1), (0, 1, 2, 3, 4, 5)])
 
-    path, path_str = oe.contract_path(expression, *edge_test4, path='optimal')
+    path, path_str = oe.contract_path(expression, *edge_test4, optimize='optimal', memory_limit='max_input')
     assert check_path(path, [(0, 1), (0, 1, 2, 3, 4, 5)])
 
 
@@ -165,17 +165,17 @@ def test_greedy_edge_cases():
     dim_dict = {k: 20 for k in expression.replace(",", "")}
     tensors = oe.helpers.build_views(expression, dimension_dict=dim_dict)
 
-    path, path_str = oe.contract_path(expression, *tensors, path='greedy')
+    path, path_str = oe.contract_path(expression, *tensors, optimize='greedy', memory_limit='max_input')
     assert check_path(path, [(0, 1, 2, 3)])
 
-    path, path_str = oe.contract_path(expression, *tensors, path='greedy', memory_limit=-1)
+    path, path_str = oe.contract_path(expression, *tensors, optimize='greedy', memory_limit=-1)
     assert check_path(path, [(0, 1), (0, 2), (0, 1)])
 
 
 def test_can_optimize_outer_products():
     a, b, c = [np.random.randn(10, 10) for _ in range(3)]
     d = np.random.randn(10, 2)
-    assert oe.contract_path("ab,cd,ef,fg", a, b, c, d, path='greedy')[0] == [(2, 3), (0, 2), (0, 1)]
+    assert oe.contract_path("ab,cd,ef,fg", a, b, c, d, optimize='greedy')[0] == [(2, 3), (0, 2), (0, 1)]
 
 
 @pytest.mark.parametrize('num_symbols', [2, 3, 26, 26 + 26, 256 - 140, 300])
@@ -186,4 +186,4 @@ def test_large_path(num_symbols):
     tensors = oe.helpers.build_views(expression, dimension_dict=dimension_dict)
 
     # Check that path construction does not crash
-    oe.contract_path(expression, *tensors, path='greedy')
+    oe.contract_path(expression, *tensors, optimize='greedy')