Repository: google/TensorNetwork

Issue:
Enable contractor contraction on disconnected graphs
When we are using tensor network to represent a tensor in the quantum physics, there is no rule that the network must be connected. for example, we can represent an exterior product from two non-connected nodes.


Discussion:
Thanks for your issue!

How would you want the final result returned? We can either

1. Outer product the final set of tensors together or 
2. Return the nodes in a set.

Option 1 would be the easiest to implement on our side. Option 2 might be difficult for the user to use, since the nodes are in a set and can be hard to distinguish. 
Well actually now that I think about it both would be amount equal in difficult to implement. I still prefer 1 as that is what I assume you actually want.
> Well actually now that I think about it both would be amount equal in difficult to implement. I still prefer 1 as that is what I assume you actually want.

1 is what I wanted in my current project. 2 might be helpful when there is a memory bottleneck.  However, if optimization is needed, one should explicitly split the graph, so i feel 2 is not very useful.

I just found there is a "tensornetwork.outer_product_final_nodes" method which might turn 2 to 1. 
Thank you very much
We should be able to do this automatically for you though. I can do this pretty easily, as all we have to do is contract each disconnected subnetwork and then just outerproduct the results.

I can add this in the next release.
So I wrote something I thought would fix it but I'm hitting a crash in the opt_einsum library. It's likely my fault, so I'll follow up on this next week.
I would also like to have this, and prefer option 1.

We should also probably have a function that identifies and returns all disconnected subnetworks from a given set of nodes.
I can add both

Resolving patch:
diff --git a/tensornetwork/contractors/opt_einsum_paths/path_contractors.py b/tensornetwork/contractors/opt_einsum_paths/path_contractors.py
--- a/tensornetwork/contractors/opt_einsum_paths/path_contractors.py
+++ b/tensornetwork/contractors/opt_einsum_paths/path_contractors.py
@@ -19,7 +19,7 @@
 # pylint: disable=line-too-long
 from tensornetwork.network_operations import check_connected, get_all_edges, get_subgraph_dangling
 # pylint: disable=line-too-long
-from tensornetwork.network_components import get_all_nondangling, contract_parallel
+from tensornetwork.network_components import get_all_nondangling, contract_parallel, contract_between
 from tensornetwork.network_components import Edge, BaseNode
 from tensornetwork.contractors.opt_einsum_paths import utils
 from typing import Any, Optional, Sequence, Iterable
@@ -50,7 +50,6 @@ def base(nodes: Iterable[BaseNode],
     Final node after full contraction.
   """
   nodes_set = set(nodes)
-  check_connected(nodes_set)
   edges = get_all_edges(nodes_set)
   #output edge order has to be determinded before any contraction
   #(edges are refreshed after contractions)
@@ -84,7 +83,7 @@ def base(nodes: Iterable[BaseNode],
   # Then apply `opt_einsum`'s algorithm
   path, nodes = utils.get_path(nodes_set, algorithm)
   for a, b in path:
-    new_node = nodes[a] @ nodes[b]
+    new_node = contract_between(nodes[a], nodes[b], allow_outer_product=True)
     nodes.append(new_node)
     nodes = utils.multi_remove(nodes, [a, b])
 
@@ -192,7 +191,7 @@ def greedy(
 
 # pylint: disable=too-many-return-statements
 def auto(
-    nodes: BaseNode,
+    nodes: Iterable[BaseNode],
     output_edge_order: Optional[Sequence[Edge]] = None,
     memory_limit: Optional[int] = None,
     ignore_edge_order: bool = False) -> BaseNode:


Test patch:
diff --git a/tensornetwork/contractors/opt_einsum_paths/path_contractors_node_test.py b/tensornetwork/contractors/opt_einsum_paths/path_contractors_node_test.py
--- a/tensornetwork/contractors/opt_einsum_paths/path_contractors_node_test.py
+++ b/tensornetwork/contractors/opt_einsum_paths/path_contractors_node_test.py
@@ -57,20 +57,6 @@ def test_trace_edge(backend, path_algorithm):
   np.testing.assert_allclose(node.tensor, np.ones(2) * 32.0)
 
 
-def test_disconnected_network(backend, path_algorithm):
-  a = Node(np.array([2, 2]), backend=backend)
-  b = Node(np.array([2, 2]), backend=backend)
-  c = Node(np.array([2, 2]), backend=backend)
-  d = Node(np.array([2, 2]), backend=backend)
-
-  # pylint: disable=pointless-statement
-  a[0] ^ b[0]
-  c[0] ^ d[0]
-  nodes = [a, b, c, d]
-  with pytest.raises(ValueError):
-    path_algorithm(nodes)
-
-
 def test_single_node(backend, path_algorithm):
   a = Node(np.ones((2, 2, 2)), backend=backend)
   # pylint: disable=pointless-statement
@@ -177,3 +163,21 @@ def test_ignore_edge_order_with_order(backend, path_algorithm):
                               ignore_edge_order=True)
 
   assert set(final_node.edges) == {e0, e1}
+
+def test_disconnected_network(backend, path_algorithm):
+  a = Node(np.eye(2), backend=backend)
+  b = Node(np.eye(2), backend=backend)
+  c = Node(np.eye(2), backend=backend)
+  d = Node(np.eye(2), backend=backend)
+  e = Node(np.eye(2), backend=backend)
+  f = Node(np.eye(2), backend=backend)
+  g = Node(np.eye(2), backend=backend)
+  a[1] ^ b[0]
+  c[0] ^ d[1]
+  e[0] ^ f[0]
+  g[0] ^ f[1]
+  final_edges = [a[0], b[1], c[1], d[0], e[1], g[1]]
+  result = path_algorithm(
+      {a, b, c, d, e, f, g}, 
+      final_edges)
+  assert result.edges == final_edges