Repository: google/TensorNetwork

Issue:
Outer product for operators
#602 brought up an interesting point of defining operators based on the outerproduct of several smaller tensors. The issue is the normally an operator has its "input axes" defined as the first N axis, and the "output axes" are the last N axes. When dot producting tensors together, the input and output axes are weaved together, forcing a user to do a nasty `reshape` or `reorder_edges` before the tensor can be used for things like the `FiniteMPS` class.

I propose a new method `tn.operator_outer_product(list_of_nodes)` that would do the outer product but maintain the edge order thats expected for operator tensors.


Discussion:
The function you are describing sounds like a version of the numpy `kron` (kronecker tensor product) but for nodes (which also produces the edge order that's expected for operator tensors). The name of the new method could be chosen to reflect this.
Good idea. I like that much better.
So here again, is the idea to use kron and then reshape?
Thank you
The idea is that kron would do the reshape for you automatically
Maybe something like `tn.operator_kron` would be a better name since `kron` doesn't do the reshaping magic.

Resolving patch:
diff --git a/tensornetwork/__init__.py b/tensornetwork/__init__.py
--- a/tensornetwork/__init__.py
+++ b/tensornetwork/__init__.py
@@ -1,7 +1,7 @@
 #pylint: disable=line-too-long
 from tensornetwork.network_components import Node, Edge, CopyNode, BaseNode, NodeCollection
 #pylint: disable=line-too-long
-from tensornetwork.network_operations import norm, conj, copy, replicate_nodes, transpose, split_node, split_node_qr, split_node_rq, split_node_full_svd, reachable, check_connected, check_correct, get_all_nodes, get_all_edges, remove_node, contract_trace_edges, get_subgraph_dangling, reduced_density, switch_backend, get_neighbors
+from tensornetwork.network_operations import norm, conj, copy, replicate_nodes, transpose, split_node, split_node_qr, split_node_rq, split_node_full_svd, reachable, check_connected, check_correct, get_all_nodes, get_all_edges, remove_node, contract_trace_edges, get_subgraph_dangling, reduced_density, switch_backend, get_neighbors, kron
 #pylint: disable=line-too-long
 from tensornetwork.network_components import contract, contract_copy_node, contract_between, outer_product, outer_product_final_nodes, contract_parallel, flatten_edges, split_edge, slice_edge, get_all_nondangling, get_all_dangling, flatten_all_edges, flatten_edges_between, get_parallel_edges, get_shared_edges
 from tensornetwork.backends.base_backend import BaseBackend
diff --git a/tensornetwork/network_operations.py b/tensornetwork/network_operations.py
--- a/tensornetwork/network_operations.py
+++ b/tensornetwork/network_operations.py
@@ -20,7 +20,7 @@
 
 #pylint: disable=useless-import-alias
 #pylint: disable=line-too-long
-from tensornetwork.network_components import BaseNode, Node, CopyNode, Edge, disconnect
+from tensornetwork.network_components import BaseNode, Node, CopyNode, Edge, disconnect, outer_product_final_nodes
 from tensornetwork.backends import backend_factory
 from tensornetwork.backends.base_backend import BaseBackend
 from tensornetwork.network_components import connect, contract_parallel
@@ -899,3 +899,40 @@ def get_neighbors(node: BaseNode) -> List[Node]:
         neighbors.append(edge.node1)
         neighbors_set.add(edge.node1)
   return neighbors
+
+def kron(nodes: Sequence[BaseNode]) -> BaseNode:
+  """Kronecker product of the given nodes.
+
+  Kronecker products of nodes is the same as the outer product, but the order
+  of the axes is different. The first half of edges of all of the nodes will
+  appear first half of edges in the resulting node, and the second half ot the
+  edges in each node will be in the second half of the resulting node.
+
+  For example, if I had two nodes  :math:`X_{ab}`,  :math:`Y_{cdef}`, and 
+  :math:`Z_{gh}`, then the resulting node would have the edges ordered 
+  :math:`R_{acdgbefh}`.
+   
+  The kronecker product is designed such that the kron of many operators is
+  itself an operator. 
+
+  Args:
+    nodes: A sequence of `BaseNode` objects.
+
+  Returns:
+    A `Node` that is the kronecker product of the given inputs. The first
+    half of the edges of this node would represent the "input" edges of the
+    operator and the last half of edges are the "output" edges of the
+    operator.
+  """
+  input_edges = []
+  output_edges = []
+  for node in nodes:
+    order = len(node.shape)
+    if order % 2 != 0:
+      raise ValueError(
+          f"All operator tensors must have an even order. "
+          f"Found tensor with order {order}")
+    input_edges += node.edges[:order//2]
+    output_edges += node.edges[order//2:]
+  result = outer_product_final_nodes(nodes, input_edges + output_edges)
+  return result


Test patch:
diff --git a/tensornetwork/tests/network_operations_test.py b/tensornetwork/tests/network_operations_test.py
--- a/tensornetwork/tests/network_operations_test.py
+++ b/tensornetwork/tests/network_operations_test.py
@@ -527,3 +527,19 @@ def test_get_neighbors_no_duplicates(backend):
     b[3] ^ b[4]
     result = tn.get_neighbors(b)
     assert result == [a, c]
+
+def test_operator_kron(backend):
+  with tn.DefaultBackend(backend):
+    X = np.array([[0, 1], [1, 0]], dtype=np.float32)
+    Z = np.array([[1, 0], [0, -1]], dtype=np.float32)
+    expected = np.kron(X, Z).reshape(2, 2, 2, 2)
+    result = tn.kron([tn.Node(X), tn.Node(Z)])
+    np.testing.assert_allclose(result.tensor, expected)
+
+def test_kron_raises(backend):
+  with tn.DefaultBackend(backend):
+    A = tn.Node(np.ones((2, 2, 2)))
+    B = tn.Node(np.ones((2, 2, 2)))
+    with pytest.raises(
+        ValueError, match="All operator tensors must have an even order."):
+      tn.kron([A, B])