Repository: google/TensorNetwork

Issue:
Support induce subgraph in the copy function
Now that we are moving away from using `TensorNetwork` object, we should still allow copying a network via passing a collection of nodes to a `tensornetwork.copy(nodes)` function. 

Along with this, it would be nice if when we passed a subset of all the nodes that it would create an induced subgraph copy of the given nodes.

By this I mean that if `a` and `b` are connected by an edge and we do `tensornetwork.copy({a})`, then the returned set of nodes should only be a copy of `a` and the edge that used to connect `a` and `b` becomes a dangling edge.


Discussion:
I can work on this.
Great!

Resolving patch:
diff --git a/tensornetwork/network_components.py b/tensornetwork/network_components.py
--- a/tensornetwork/network_components.py
+++ b/tensornetwork/network_components.py
@@ -28,7 +28,7 @@
 
 string_type = h5py.special_dtype(vlen=str)
 Tensor = Any
-# This is required because of the circular dependancy between
+# This is required because of the circular dependency between
 # network_components.py and network.py types.
 TensorNetwork = Any
 
diff --git a/tensornetwork/network_operations.py b/tensornetwork/network_operations.py
--- a/tensornetwork/network_operations.py
+++ b/tensornetwork/network_operations.py
@@ -96,7 +96,8 @@ def copy(nodes: Iterable[BaseNode],
   """Copy the given nodes and their edges.
 
   This will return a dictionary linking original nodes/edges 
-  to their copies.
+  to their copies. If nodes A and B are connected but only A is passed in to be
+  copied, the edge between them will become a dangling edge.
 
   Args:
     nodes: An `Iterable` (Usually a `List` or `Set`) of `Nodes`.
@@ -130,20 +131,30 @@ def copy(nodes: Iterable[BaseNode],
   for edge in get_all_edges(nodes):
     node1 = edge.node1
     axis1 = edge.node1.get_axis_number(edge.axis1)
-
-    if not edge.is_dangling():
-      node2 = edge.node2
-      axis2 = edge.node2.get_axis_number(edge.axis2)
-      new_edge = Edge(node_dict[node1], axis1, edge.name, node_dict[node2],
-                      axis2)
-      new_edge.set_signature(edge.signature)
-    else:
+    # edge dangling or node2 does not need to be copied
+    if edge.is_dangling() or edge.node2 not in node_dict:
       new_edge = Edge(node_dict[node1], axis1, edge.name)
-
-    node_dict[node1].add_edge(new_edge, axis1)
-    if not edge.is_dangling():
+      node_dict[node1].add_edge(new_edge, axis1)
+      edge_dict[edge] = new_edge
+      continue
+
+    node2 = edge.node2
+    axis2 = edge.node2.get_axis_number(edge.axis2)
+    # copy node2 but not node1
+    if node1 not in node_dict:
+      new_edge = Edge(node_dict[node2], axis2, edge.name)
       node_dict[node2].add_edge(new_edge, axis2)
+      edge_dict[edge] = new_edge
+      continue
+
+    # both nodes should be copied
+    new_edge = Edge(node_dict[node1], axis1, edge.name, node_dict[node2],
+                    axis2)
+    new_edge.set_signature(edge.signature)
+    node_dict[node2].add_edge(new_edge, axis2)
+    node_dict[node1].add_edge(new_edge, axis1)
     edge_dict[edge] = new_edge
+
   return node_dict, edge_dict
 
 


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
@@ -78,6 +78,33 @@ def test_tnwork_copy_identities(backend):
     assert not edge_dict[edge] is edge
 
 
+def test_tnwork_copy_subgraph(backend):
+  a = tn.Node(np.random.rand(3, 3, 3), name='a', backend=backend)
+  b = tn.Node(np.random.rand(3, 3, 3), name='b', backend=backend)
+  c = tn.Node(np.random.rand(3, 3, 3), name='c', backend=backend)
+  a[0] ^ b[1]
+  edge2 = b[2] ^ c[0]
+  node_dict, edge_dict = tn.copy({a, b})
+  cut_edge = edge_dict[edge2]
+  assert cut_edge.is_dangling()
+  assert cut_edge.axis1 == 2
+  assert cut_edge.get_nodes() == [node_dict[b], None]
+  assert len(a.get_all_nondangling()) == 1
+
+def test_tnwork_copy_subgraph_2(backend):
+  a = tn.Node(np.random.rand(3, 3, 3), name='a', backend=backend)
+  b = tn.Node(np.random.rand(3, 3, 3), name='b', backend=backend)
+  c = tn.Node(np.random.rand(3, 3, 3), name='c', backend=backend)
+  a[0] ^ b[1]
+  edge2 = c[2] ^ b[0]
+  node_dict, edge_dict = tn.copy({a, b})
+  cut_edge = edge_dict[edge2]
+  assert cut_edge.is_dangling()
+  assert cut_edge.axis1 == 0
+  assert cut_edge.get_nodes() == [node_dict[b], None]
+  assert len(a.get_all_nondangling()) == 1
+
+
 def test_connect_axis_names(backend):
   a = tn.Node(np.ones((3,)), name="a", axis_names=["one"], backend=backend)
   b = tn.Node(np.ones((3,)), name="b", axis_names=["one"], backend=backend)