Repository: google/TensorNetwork

Issue:
If a single-node contraction has an output_edge_order, its edges should be reordered
Currently if a contractor is passed in a single node, nothing is done.

https://github.com/google/TensorNetwork/blob/a1bcaed7fc1ab9204ca1b48053873562cadc3c9e/tensornetwork/contractors/opt_einsum_paths/path_contractors.py#L74-L76

But if `output_edge_order` is passed in, I would expect the edges to be reordered accordingly. If this is the desired behavior I'm happy to help with adding the fix!
If a single-node contraction has an output_edge_order, its edges should be reordered
Currently if a contractor is passed in a single node, nothing is done.

https://github.com/google/TensorNetwork/blob/a1bcaed7fc1ab9204ca1b48053873562cadc3c9e/tensornetwork/contractors/opt_einsum_paths/path_contractors.py#L74-L76

But if `output_edge_order` is passed in, I would expect the edges to be reordered accordingly. If this is the desired behavior I'm happy to help with adding the fix!


Discussion:
Hey @verult, thanks for noticing! This is a bug and should be fixed. PR's are very welcome!
Yes def a bug. Also seems like a really nice first PR for a 20%er ;)
Hey @verult, thanks for noticing! This is a bug and should be fixed. PR's are very welcome!
Yes def a bug. Also seems like a really nice first PR for a 20%er ;)

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
@@ -73,7 +73,7 @@ def _base_nodes(nodes: Iterable[BaseNode],
 
   if len(nodes_set) == 1:
     # There's nothing to contract.
-    return list(nodes_set)[0]
+    return list(nodes_set)[0].reorder_edges(output_edge_order)
 
   # Then apply `opt_einsum`'s algorithm
   path, nodes = utils.get_path(nodes_set, algorithm)
@@ -154,7 +154,7 @@ def base(nodes: Union[TensorNetwork, Iterable[BaseNode]],
 
 def optimal(
     nodes: Union[TensorNetwork, Iterable[BaseNode]],
-    output_edge_order: Sequence[Edge] = None,
+    output_edge_order: Optional[Sequence[Edge]] = None,
     memory_limit: Optional[int] = None):  # -> Union[BaseNode, TensorNetwork]:
   """Optimal contraction order via `opt_einsum`.
 
@@ -180,7 +180,7 @@ def optimal(
 
 
 def branch(nodes: Union[TensorNetwork, Iterable[BaseNode]],
-           output_edge_order: Sequence[Edge] = None,
+           output_edge_order: Optional[Sequence[Edge]] = None,
            memory_limit: Optional[int] = None,
            nbranch: Optional[int] = None):  # -> Union[BaseNode, TensorNetwork]:
   """Branch contraction path via `opt_einsum`.
@@ -213,7 +213,7 @@ def branch(nodes: Union[TensorNetwork, Iterable[BaseNode]],
 
 def greedy(
     nodes: Union[TensorNetwork, Iterable[BaseNode]],
-    output_edge_order: Sequence[Edge] = None,
+    output_edge_order: Optional[Sequence[Edge]] = None,
     memory_limit: Optional[int] = None):  # -> Union[BaseNode, TensorNetwork]:
   """Greedy contraction path via `opt_einsum`.
 
@@ -243,7 +243,7 @@ def greedy(
 # pylint: disable=too-many-return-statements
 def auto(
     nodes: Union[TensorNetwork, BaseNode],
-    output_edge_order: Sequence[Edge] = None,
+    output_edge_order: Optional[Sequence[Edge]] = None,
     memory_limit: Optional[int] = None):  # -> Union[TensorNetwork, BaseNode]:
   """Chooses one of the above algorithms according to network size.
 
@@ -279,7 +279,10 @@ def auto(
                          "has to be provided.")
 
     edges = get_all_nondangling(_nodes)
-    final_node = contract_parallel(edges.pop())
+    if edges:
+      final_node = contract_parallel(edges.pop())
+    else:
+      final_node = list(_nodes)[0]
     final_node.reorder_edges(output_edge_order)
     if isinstance(nodes, TensorNetwork):
       node = list(_nodes)[0]
@@ -303,7 +306,7 @@ def auto(
 def custom(
     nodes: Union[TensorNetwork, Iterable[BaseNode]],
     optimizer: Any,
-    output_edge_order: Sequence[Edge] = None,
+    output_edge_order: Optional[Sequence[Edge]] = None,
     memory_limit: Optional[int] = None):  #x -> Union[BaseNode, TensorNetwork]:
   """Uses a custom path optimizer created by the user to calculate paths.
 


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
@@ -134,3 +134,11 @@ def test_multiple_partial_contractions(backend, path_algorithm):
   result = path_algorithm({ab, cd})
   np.testing.assert_allclose(
       result.tensor, np.trace(a_tensor @ b_tensor @ c_tensor @ d_tensor))
+
+
+def test_single_node_reorder(backend, path_algorithm):
+  a = tn.Node(np.arange(4).reshape((2, 2)), backend=backend)
+  expected_edge_order = [a[1], a[0]]
+  result = path_algorithm({a}, expected_edge_order)
+  assert result.edges == expected_edge_order
+  np.testing.assert_allclose(result.tensor, np.arange(4).reshape((2, 2)).T)