Repository: dgasmith/opt_einsum

Issue:
let contract_expression use interleaved input
Currently, ``contract_expression`` only accepts the 'equation' style input. This is an easy fix that I will do shortly. As part of that I can flesh out the docs on input (#70), unless you are already planning that @dgasmith ?

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/dgasmith/opt_einsum/pull/72?src=pr&el=h1) Report
> Merging [#72](https://codecov.io/gh/dgasmith/opt_einsum/pull/72?src=pr&el=desc) into [master](https://codecov.io/gh/dgasmith/opt_einsum/commit/7daf290b236d3f464845152b25ee58509410d976?src=pr&el=desc) will **increase** coverage by `0.18%`.
> The diff coverage is `100%`.


dgasmith: This pull request **introduces 1 alert** when merging aa47e5b549d7ec464bf975db3815f0444f84caf4 into 7daf290b236d3f464845152b25ee58509410d976 - [view on LGTM.com](https://lgtm.com/projects/g/dgasmith/opt_einsum/rev/pr-eed860d86e7d7251d2af7be5199ad3c1bf274446)

**new alerts:**

* 1 for Syntax error

---

*Comment posted by [LGTM.com](https://lgtm.com)*

Patch:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 5d0c74c..612c602 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -123,6 +123,7 @@ Table of Contents
    :caption: Getting Started
 
    install
+   input_format
    backends
    reusing_paths
    sharing_intermediates
@@ -147,6 +148,5 @@ Table of Contents
    :maxdepth: 1
    :caption: Help & Reference:
 
-   input_format
    api
    changelog
diff --git a/docs/source/input_format.rst b/docs/source/input_format.rst
index fa7b1e7..937d744 100644
--- a/docs/source/input_format.rst
+++ b/docs/source/input_format.rst
@@ -2,13 +2,72 @@
 Input Format
 ============
 
-The ``opt_einsum`` package is a drop-in replacement for the ``np.einsum`` function
-and supports all input formats that ``np.einsum`` supports.
+The ``opt_einsum`` package is a drop-in replacement for the ``np.einsum``
+function and supports all input formats that ``np.einsum`` supports. There are
+two styles of input accepted, a basic introduction to which can be found in the
+documentation for :func:`numpy.einsum`. In addition to this, ``opt_einsum``
+extends the allowed index labels to unicode or arbitrary hashable, comparable
+objects in order to handle large contractions with many indices.
 
-TBA: "Normal" inputs
 
-In ``opt_einsum``, you can also put anything hashable and comparable such as `str` in the subscript list.
-Note ``np.einsum`` currently does not support this syntax. A simple example of the syntax is:
+'Equation' Input
+----------------
+
+As with :func:`numpy.einsum`, here you specify an equation as a string,
+followed by the array arguments:
+
+.. code:: python
+
+    >>> eq = 'ijk,jkl->li'
+    >>> x, y = np.random.rand(2, 3, 4), np.random.rand(3, 4, 5)
+    >>> z = oe.contract(eq, x, y)
+    >>> z.shape
+    (5, 2)
+
+However, in addition to the standard alphabet, ``opt_einsum`` also supports
+unicode characters:
+
+.. code:: python
+
+    >>> eq = "αβγ,βγδ->δα"
+    >>> oe.contract(eq, x, y).shape
+    (5, 2)
+
+This enables access to thousands of possible index labels. One way to access
+these programmatically is through the function
+:func:`~opt_einsum.parser.get_symbol`:
+
+    >>> oe.get_symbol(805)
+    'α'
+
+which maps an ``int`` to a unicode characater. Note that as with
+:func:`numpy.einsum` if the output is not specified with ``->`` it will default
+to the sorted order of all indices appearing once:
+
+.. code:: python
+
+    >>> eq = "αβγ,βγδ"  # "->αδ" is implicit
+    >>> oe.contract(eq, x, y).shape
+    (2, 5)
+
+
+'Interleaved' Input
+-------------------
+
+The other input format is to 'interleave' the array arguments with their index
+labels ('subscripts') in pairs, optionally specifying the output indices as a
+final argument. As with :func:`numpy.einsum`, integers are allowed as these
+index labels:
+
+.. code:: python
+
+    >>> oe.contract(x, [1, 2, 3], y, [2, 3, 4], [4, 1]).shape
+    >>> (5, 2)
+
+with the default output order again specified by the sorted order of indices
+appearing once. However, unlike :func:`numpy.einsum`, in ``opt_einsum`` you can
+also put *anything* hashable and comparable such as `str` in the subscript list.
+A simple example of this syntax is:
 
 .. code:: python
 
@@ -17,8 +76,7 @@ Note ``np.einsum`` currently does not support this syntax. A simple example of t
     array([[4.]])
 
 The subscripts need to be hashable so that ``opt_einsum`` can efficiently process them, and
-they should also be comparable because in this way we can keep a consistent behavior between integer 
-subscripts and other types of subscripts. For example:
+they should also be comparable so as to allow a default sorted output. For example:
 
 .. code:: python
 
diff --git a/opt_einsum/contract.py b/opt_einsum/contract.py
index 4a42176..6d4f26b 100644
--- a/opt_einsum/contract.py
+++ b/opt_einsum/contract.py
@@ -805,6 +805,9 @@ def contract_expression(subscripts, *shapes, **kwargs):
             raise ValueError("'{}' should only be specified when calling a "
                              "`ContractExpression`, not when building it.".format(arg))
 
+    if not isinstance(subscripts, compat.strings):
+        subscripts, shapes = parser.convert_interleaved_input((subscripts,) + shapes)
+
     kwargs['_gen_expression'] = True
 
     # build dict of constant indices mapped to arrays
diff --git a/opt_einsum/parser.py b/opt_einsum/parser.py
index 8c93213..bb15846 100644
--- a/opt_einsum/parser.py
+++ b/opt_einsum/parser.py
@@ -208,6 +208,43 @@ def convert_subscripts(old_sub, symbol_map):
     return new_sub
 
 
+def convert_interleaved_input(operands):
+    """Convert 'interleaved' input to standard einsum input.
+    """
+    tmp_operands = list(operands)
+    operand_list = []
+    subscript_list = []
+    for p in range(len(operands) // 2):
+        operand_list.append(tmp_operands.pop(0))
+        subscript_list.append(tmp_operands.pop(0))
+
+    output_list = tmp_operands[-1] if len(tmp_operands) else None
+    operands = [possibly_convert_to_numpy(x) for x in operand_list]
+
+    # build a map from user symbols to single-character symbols based on `get_symbol`
+    # The map retains the intrinsic order of user symbols
+    try:
+        # collect all user symbols
+        symbol_set = set(itertools.chain.from_iterable(subscript_list))
+
+        # remove Ellipsis because it can not be compared with other objects
+        symbol_set.discard(Ellipsis)
+
+        # build the map based on sorted user symbols, retaining the order we lost in the `set`
+        symbol_map = {symbol: get_symbol(idx) for idx, symbol in enumerate(sorted(symbol_set))}
+
+    except TypeError:  # unhashable or uncomparable object
+        raise TypeError("For this input type lists must contain either Ellipsis "
+                        "or hashable and comparable object (e.g. int, str).")
+
+    subscripts = ','.join(convert_subscripts(sub, symbol_map) for sub in subscript_list)
+    if output_list is not None:
+        subscripts += "->"
+        subscripts += convert_subscripts(output_list, symbol_map)
+
+    return subscripts, operands
+
+
 def parse_einsum_input(operands):
     """
     A reproduction of einsum c side einsum parsing in python.
@@ -242,36 +279,7 @@ def parse_einsum_input(operands):
         operands = [possibly_convert_to_numpy(x) for x in operands[1:]]
 
     else:
-        tmp_operands = list(operands)
-        operand_list = []
-        subscript_list = []
-        for p in range(len(operands) // 2):
-            operand_list.append(tmp_operands.pop(0))
-            subscript_list.append(tmp_operands.pop(0))
-
-        output_list = tmp_operands[-1] if len(tmp_operands) else None
-        operands = [possibly_convert_to_numpy(x) for x in operand_list]
-
-        # build a map from user symbols to single-character symbols based on `get_symbol`
-        # The map retains the intrinsic order of user symbols
-        try:
-            # collect all user symbols
-            symbol_set = set(itertools.chain.from_iterable(subscript_list))
-        except TypeError:  # unhashable object
-            raise TypeError("For this input type lists must contain either Ellipsis or hashable and comparable object (e.g. int, str)")
-        # remove Ellipsis because it can not be compared with other objects
-        if Ellipsis in symbol_set:
-            symbol_set.remove(Ellipsis)
-        try:
-            # build the map based on sorted user symbols, retaining the order we lost in the `set`
-            symbol_map = {symbol: get_symbol(idx) for idx, symbol in enumerate(sorted(symbol_set))}
-        except TypeError:  # uncomparable object
-            raise TypeError("For this input type lists must contain either Ellipsis or hashable and comparable object (e.g. int, str)")
-
-        subscripts = ','.join(convert_subscripts(sub, symbol_map) for sub in subscript_list)
-        if output_list is not None:
-            subscripts += "->"
-            subscripts += convert_subscripts(output_list, symbol_map)
+        subscripts, operands = convert_interleaved_input(operands)
 
     # Check for proper "->"
     if ("-" in subscripts) or (">" in subscripts):


Test patch:
diff --git a/opt_einsum/tests/test_contract.py b/opt_einsum/tests/test_contract.py
index dbbc39e..3371876 100644
--- a/opt_einsum/tests/test_contract.py
+++ b/opt_einsum/tests/test_contract.py
@@ -194,6 +194,15 @@ def test_contract_expressions(string, optimize, use_blas, out_spec):
     assert string in expr.__str__()
 
 
+def test_contract_expression_interleaved_input():
+    x, y, z = (np.random.randn(2, 2) for _ in 'xyz')
+    expected = np.einsum(x, [0, 1], y, [1, 2], z, [2, 3], [3, 0])
+    xshp, yshp, zshp = ((2, 2) for _ in 'xyz')
+    expr = contract_expression(xshp, [0, 1], yshp, [1, 2], zshp, [2, 3], [3, 0])
+    out = expr(x, y, z)
+    assert np.allclose(out, expected)
+
+
 @pytest.mark.parametrize("string,constants", [
     ('hbc,bdef,cdkj,ji,ikeh,lfo', [1, 2, 3, 4]),
     ('bdef,cdkj,ji,ikeh,hbc,lfo', [0, 1, 2, 3]),