Repository: google/TensorNetwork

Issue:
split_node_... mutates its input
`split_node_full_svd(node, [node[0], node[2]], [node[1]])` mutates the shape of `node`. 
Other `split_node_...` functions behave similarly



Discussion:
I would prefer `reorder_edges` to return a copy instead of mutating the object. This could break a lot of code though. What's your opinion @Thenerdstation?
Well, you can't exactly make a copy since those edges are for that node.

What would be the recourse here? Split_node refreshes the edges anyway.
I think a better option would be to return a reshaped underlying tensor instead of reordering the edges on the node itself. We could add a method that's similar to `reorder_edges` that returns a tensor instead of the node and leaves the node untouched.
yes that would work 

Resolving patch:
diff --git a/tensornetwork/network_components.py b/tensornetwork/network_components.py
--- a/tensornetwork/network_components.py
+++ b/tensornetwork/network_components.py
@@ -292,6 +292,18 @@ def reorder_axes(self, perm: List[int]) -> "BaseNode":
       self.axis_names = tmp_axis_names
     return self
 
+  def tensor_from_edge_order(self, perm: List["Edge"]) -> "BaseNode":
+    order = []
+    for edge in perm:
+      if edge.node1 is self:
+        order.append(edge.axis1)
+      elif edge.node2 is self:
+        order.append(edge.axis2)
+      else:
+        raise ValueError("edge {} is not connected to node {}".format(
+            edge.name, self.name))
+    return self.backend.transpose(self.tensor, order)
+
   def get_axis_number(self, axis: Union[Text, int]) -> int:
     """Get the axis number for a given axis name or value."""
     if isinstance(axis, int):
diff --git a/tensornetwork/network_operations.py b/tensornetwork/network_operations.py
--- a/tensornetwork/network_operations.py
+++ b/tensornetwork/network_operations.py
@@ -284,10 +284,10 @@ def split_node(
     right_axis_names = None
 
   backend = node.backend
-  node.reorder_edges(left_edges + right_edges)
+  transp_tensor = node.tensor_from_edge_order(left_edges + right_edges)
 
   u, s, vh, trun_vals = backend.svd_decomposition(
-      node.tensor,
+      transp_tensor,
       len(left_edges),
       max_singular_values,
       max_truncation_err,
@@ -298,15 +298,25 @@ def split_node(
 
   left_node = Node(
       u_s, name=left_name, axis_names=left_axis_names, backend=backend)
+
+  left_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in left_edges
+  ]
   for i, edge in enumerate(left_edges):
     left_node.add_edge(edge, i)
-    edge.update_axis(i, node, i, left_node)
+    edge.update_axis(left_axes_order[i], node, i, left_node)
+
   right_node = Node(
       vh_s, name=right_name, axis_names=right_axis_names, backend=backend)
+
+  right_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in right_edges
+  ]
   for i, edge in enumerate(right_edges):
     # i + 1 to account for the new edge.
     right_node.add_edge(edge, i + 1)
-    edge.update_axis(i + len(left_edges), node, i + 1, right_node)
+    edge.update_axis(right_axes_order[i], node, i + 1, right_node)
+
   connect(left_node.edges[-1], right_node.edges[0], name=edge_name)
   node.fresh_edges(node.axis_names)
   return left_node, right_node, trun_vals
@@ -369,21 +379,33 @@ def split_node_qr(
     right_axis_names = None
 
   backend = node.backend
-  node.reorder_edges(left_edges + right_edges)
-  q, r = backend.qr_decomposition(node.tensor, len(left_edges))
+  transp_tensor = node.tensor_from_edge_order(left_edges + right_edges)
+
+  q, r = backend.qr_decomposition(transp_tensor, len(left_edges))
   left_node = Node(
       q, name=left_name, axis_names=left_axis_names, backend=backend)
+
+  left_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in left_edges
+  ]
   for i, edge in enumerate(left_edges):
     left_node.add_edge(edge, i)
-    edge.update_axis(i, node, i, left_node)
+    edge.update_axis(left_axes_order[i], node, i, left_node)
+
   right_node = Node(
       r, name=right_name, axis_names=right_axis_names, backend=backend)
+
+  right_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in right_edges
+  ]
   for i, edge in enumerate(right_edges):
     # i + 1 to account for the new edge.
     right_node.add_edge(edge, i + 1)
-    edge.update_axis(i + len(left_edges), node, i + 1, right_node)
+    edge.update_axis(right_axes_order[i], node, i + 1, right_node)
+
   connect(left_node.edges[-1], right_node.edges[0], name=edge_name)
   node.fresh_edges(node.axis_names)
+
   return left_node, right_node
 
 
@@ -444,19 +466,31 @@ def split_node_rq(
     left_axis_names = None
     right_axis_names = None
   backend = node.backend
-  node.reorder_edges(left_edges + right_edges)
-  r, q = backend.rq_decomposition(node.tensor, len(left_edges))
+  transp_tensor = node.tensor_from_edge_order(left_edges + right_edges)
+
+  r, q = backend.rq_decomposition(transp_tensor, len(left_edges))
   left_node = Node(
       r, name=left_name, axis_names=left_axis_names, backend=backend)
+
+  left_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in left_edges
+  ]
   for i, edge in enumerate(left_edges):
     left_node.add_edge(edge, i)
-    edge.update_axis(i, node, i, left_node)
+    edge.update_axis(left_axes_order[i], node, i, left_node)
+
   right_node = Node(
       q, name=right_name, axis_names=right_axis_names, backend=backend)
+
+  right_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in right_edges
+  ]
+
   for i, edge in enumerate(right_edges):
     # i + 1 to account for the new edge.
     right_node.add_edge(edge, i + 1)
-    edge.update_axis(i + len(left_edges), node, i + 1, right_node)
+    edge.update_axis(right_axes_order[i], node, i + 1, right_node)
+
   connect(left_node.edges[-1], right_node.edges[0], name=edge_name)
   node.fresh_edges(node.axis_names)
   return left_node, right_node
@@ -560,10 +594,10 @@ def split_node_full_svd(
     right_axis_names = None
 
   backend = node.backend
+  transp_tensor = node.tensor_from_edge_order(left_edges + right_edges)
 
-  node.reorder_edges(left_edges + right_edges)
   u, s, vh, trun_vals = backend.svd_decomposition(
-      node.tensor,
+      transp_tensor,
       len(left_edges),
       max_singular_values,
       max_truncation_err,
@@ -579,13 +613,20 @@ def split_node_full_svd(
   right_node = Node(
       vh, name=right_name, axis_names=right_axis_names, backend=backend)
 
+  left_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in left_edges
+  ]
   for i, edge in enumerate(left_edges):
     left_node.add_edge(edge, i)
-    edge.update_axis(i, node, i, left_node)
+    edge.update_axis(left_axes_order[i], node, i, left_node)
+
+  right_axes_order = [
+      edge.axis1 if edge.node1 is node else edge.axis2 for edge in right_edges
+  ]
   for i, edge in enumerate(right_edges):
     # i + 1 to account for the new edge.
     right_node.add_edge(edge, i + 1)
-    edge.update_axis(i + len(left_edges), node, i + 1, right_node)
+    edge.update_axis(right_axes_order[i], node, i + 1, right_node)
   connect(
       left_node.edges[-1], singular_values_node.edges[0], name=left_edge_name)
   connect(


Test patch:
diff --git a/tensornetwork/tests/network_components_free_test.py b/tensornetwork/tests/network_components_free_test.py
--- a/tensornetwork/tests/network_components_free_test.py
+++ b/tensornetwork/tests/network_components_free_test.py
@@ -1608,3 +1608,17 @@ def test_remove_edges_trace_raises_value_error(single_node_edge):
   edge = tn.connect(node[1], node[2])
   with pytest.raises(ValueError):
     _remove_edges(edge, node, node, node)  # pytype: disable=wrong-arg-types
+
+
+def test_tensor_from_edge_order(backend):
+  node = tn.Node(np.random.rand(2, 3, 4), backend=backend)
+  order = [2, 0, 1]
+  transp_tensor = node.tensor_from_edge_order([node[o] for o in order])
+  np.testing.assert_allclose(transp_tensor.shape, [4, 2, 3])
+
+
+def test_tensor_from_edge_order_raises(backend):
+  node = tn.Node(np.random.rand(2, 3, 4), backend=backend)
+  node2 = tn.Node(np.random.rand(2, 3, 4), backend=backend)
+  with pytest.raises(ValueError):
+    node.tensor_from_edge_order([node[1], node2[1], node[2]])
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
@@ -52,7 +52,7 @@ def test_split_node_full_svd_names(backend):
 
 def test_split_node_relative_tolerance(backend):
   absolute = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend)
-  relative = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend) 
+  relative = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend)
   max_truncation_err = 0.2
 
   _, _, trunc_sv_absolute, = tn.split_node(
@@ -73,7 +73,7 @@ def test_split_node_relative_tolerance(backend):
 
 def test_split_node_full_svd_relative_tolerance(backend):
   absolute = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend)
-  relative = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend) 
+  relative = tn.Node(np.diag([2.0, 1.0, 0.2, 0.1]), backend=backend)
   max_truncation_err = 0.2
 
   _, _, _, trunc_sv_absolute, = tn.split_node_full_svd(
@@ -371,6 +371,7 @@ def test_reduced_density_nondangling(backend):
   with pytest.raises(ValueError, match=err_msg):
     tn.reduced_density([a[0], b[1], c[1]])
 
+
 def test_reduced_density_contraction(backend):
   if backend == "pytorch":
     pytest.skip("pytorch doesn't support complex numbers")
@@ -475,3 +476,27 @@ def test_switch_backend_raises_error(backend):
   a.backend = BaseBackend()
   with pytest.raises(NotImplementedError):
     tn.switch_backend({a}, backend)
+
+
+def test_split_node_orig_shape(backend):
+  n1 = tn.Node(np.random.rand(3, 4, 5), backend=backend)
+  tn.split_node(n1, [n1[0], n1[2]], [n1[1]])
+  np.testing.assert_allclose(n1.shape, (3, 4, 5))
+
+
+def test_split_node_full_svd_orig_shape(backend):
+  n1 = tn.Node(np.random.rand(3, 4, 5), backend=backend)
+  tn.split_node_full_svd(n1, [n1[0], n1[2]], [n1[1]])
+  np.testing.assert_allclose(n1.shape, (3, 4, 5))
+
+
+def test_split_node_rq_orig_shape(backend):
+  n1 = tn.Node(np.random.rand(3, 4, 5), backend=backend)
+  tn.split_node_rq(n1, [n1[0], n1[2]], [n1[1]])
+  np.testing.assert_allclose(n1.shape, (3, 4, 5))
+
+
+def test_split_node_qr_orig_shape(backend):
+  n1 = tn.Node(np.random.rand(3, 4, 5), backend=backend)
+  tn.split_node_qr(n1, [n1[0], n1[2]], [n1[1]])
+  np.testing.assert_allclose(n1.shape, (3, 4, 5))
diff --git a/tensornetwork/tests/split_node_symmetric_test.py b/tensornetwork/tests/split_node_symmetric_test.py
--- a/tensornetwork/tests/split_node_symmetric_test.py
+++ b/tensornetwork/tests/split_node_symmetric_test.py
@@ -89,15 +89,16 @@ def test_split_node_mixed_order(dtype, num_charges):
   tn.check_correct({left, right})
   actual = left @ right
   np.testing.assert_allclose(actual.tensor.shape, (2, 4, 6, 3, 5))
-  np.testing.assert_allclose(a.tensor.shape, (2, 4, 6, 3, 5))
+  np.testing.assert_allclose(a.tensor.shape, (2, 3, 4, 5, 6))
 
   np.testing.assert_allclose(left.tensor.data, 0)
   np.testing.assert_allclose(right.tensor.data, 0)
   np.testing.assert_allclose(left.tensor.shape[0:3], (2, 4, 6))
   np.testing.assert_allclose(right.tensor.shape[1:], (3, 5))
-
+  new_order = [0, 2, 4, 1, 3]
   assert np.all([
-      charge_equal(a.tensor.charges[n][0], actual.tensor.charges[n][0])
+      charge_equal(a.tensor.charges[new_order[n]][0],
+                   actual.tensor.charges[n][0])
       for n in range(len(a.tensor._charges))
   ])