Repository: google/TensorNetwork

Issue:
Einsum support?
Should we extend our API to support einsum equations? It could potentially make connecting nodes much less verbose. However, I question whether anyone would want to use `tn.einsum` over say `np.einsum`. Perhaps we could support only doing the left side of the equation?


Discussion:
Yeah, it might be nice for someone to copy and paste an einsum expression and build the explicit tensor network from it (like one can currently do with `ncon_network()`). That way they can easily e.g. generate a picture of their network, or use it as a component of a larger network, etc.

Resolving patch:
diff --git a/tensornetwork/__init__.py b/tensornetwork/__init__.py
--- a/tensornetwork/__init__.py
+++ b/tensornetwork/__init__.py
@@ -55,7 +55,7 @@
 from tensornetwork.version import __version__
 from tensornetwork.visualization.graphviz import to_graphviz
 from tensornetwork import contractors
-from tensornetwork.utils import load_nodes, save_nodes
+from tensornetwork.utils import load_nodes, save_nodes, from_topology
 from tensornetwork.matrixproductstates.infinite_mps import InfiniteMPS
 from tensornetwork.matrixproductstates.finite_mps import FiniteMPS
 from tensornetwork.matrixproductstates.dmrg import FiniteDMRG
diff --git a/tensornetwork/utils.py b/tensornetwork/utils.py
--- a/tensornetwork/utils.py
+++ b/tensornetwork/utils.py
@@ -14,7 +14,7 @@
 
 import h5py
 from tensornetwork.component_factory import get_component
-from tensornetwork.network_components import Edge, AbstractNode
+from tensornetwork.network_components import Edge, AbstractNode, Node
 from tensornetwork.network_operations import reachable, get_all_edges
 from typing import List, Union, BinaryIO
 import numpy as np
@@ -120,3 +120,37 @@ def load_nodes(path: str) -> List[AbstractNode]:
     node.set_name(node_names[node.name])
 
   return nodes_list
+
+def from_topology(topology, tensors, backend=None):
+  """Create and connect new `tn.Node`s by the given einsum-like topology.
+  
+  Example:
+    ```
+    a, b, c = tn.from_topology("xy,yz,zx", [a, b, c])
+    ```
+  Args:
+    topology: A string that defines the topology. Should be like
+      the left side of an einsum expression.
+    tensors: The tensors needed to create the nodes.
+
+  Returns:
+    A list of Nodes.
+  """
+  edge_dict = {}
+  nodes = []
+  split_list = topology.split(",")
+  if len(split_list) != len(tensors):
+    raise ValueError("topology and number of tensors is mismatched")
+  for local_axes, tensor in zip(split_list, tensors):
+    local_axes_list = list(local_axes)
+    if len(local_axes_list) != len(tensor.shape):
+      raise ValueError(f"{local_axes} does not match shape {tensor.shape}")
+    new_node = Node(tensor, axis_names=local_axes_list, backend=backend)
+    for c in local_axes:
+      if c in edge_dict:
+        edge_dict[c] = edge_dict[c] ^ new_node[c]
+      else:
+        edge_dict[c] = new_node[c]
+    nodes.append(new_node)
+  return nodes     
+      


Test patch:
diff --git a/tensornetwork/tests/network_test.py b/tensornetwork/tests/network_test.py
--- a/tensornetwork/tests/network_test.py
+++ b/tensornetwork/tests/network_test.py
@@ -591,3 +591,20 @@ def test_remove_node(backend):
   broken_edges_by_name, broken_edges_by_axis = tn.remove_node(b)
   assert broken_edges_by_name == {"0": a[0]}
   assert broken_edges_by_axis == {0: a[0]}
+
+def test_from_topology(backend):
+  #pylint: disable=unbalanced-tuple-unpacking
+  x, y, z = tn.from_topology(
+      "abc,bceg,adef", 
+      [np.ones((2,) * n) for n in [3, 4, 4]],
+      backend=backend)
+  assert x.axis_names == ['a', 'b', 'c']
+  assert y.axis_names == ['b', 'c', 'e', 'g']
+  assert z.axis_names == ['a', 'd', 'e', 'f']
+  assert x['a'] is z['a']
+  assert x['b'] is y['b']
+  assert x['c'] is y['c']
+  assert z['d'].is_dangling()
+  assert y['e'] is z['e']
+  assert z['f'].is_dangling()
+  assert y['g'].is_dangling()