Repository: google/TensorNetwork

Issue:
Implement tn.reduced_density operation
I want an easy want to calculate reduced density operations. The API would look like.

```python
nodes_dict, edges_dict = tn.reduced_density(traced_out_edges)
```

Underneath, we would copy/conjugate all of the nodes in reachable by the `traced_out_edges` and return a dictionary of the old nodes/edges to the copied nodes/edges. We would then connect all of the edges in `traced_out_edges` to their copies.


Discussion:
Hi, I would like to work on this issue. As this is my first time contributing to the open source project, I would like to have a little guidance on what has to be done.

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, 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
+from tensornetwork.network_operations import norm, conj, copy, 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
 #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, get_all_nondangling, get_all_dangling, flatten_all_edges, flatten_edges_between, get_parallel_edges, get_shared_edges
 
diff --git a/tensornetwork/network_operations.py b/tensornetwork/network_operations.py
--- a/tensornetwork/network_operations.py
+++ b/tensornetwork/network_operations.py
@@ -729,6 +729,36 @@ def contract_trace_edges(node: BaseNode) -> BaseNode:
   raise ValueError('`node` has no trace edges')
 
 
+def reduced_density(traced_out_edges: Iterable[Edge]) -> Tuple[dict, dict]:
+  """
+  Given a list of dangling edges, make a copy of the reachable nodes and their edges.
+  Connects each given edge to its copy.
+
+  The traced out edges in `edge_dict` will be the newly non-dangling edges created.
+
+  Args:
+    traced_out_edges: A list of dangling edges
+  Returns:
+    A tuple containing:
+      node_dict: A dictionary mapping the nodes to their copies.
+      edge_dict: A dictionary mapping the edges to their copies.
+  """
+
+  if list(filter(lambda x: not x.is_dangling(), traced_out_edges)):
+    raise ValueError("traced_out_edges must only include dangling edges!")
+
+  # Get all reachable nodes.
+  old_nodes = reachable(get_all_nodes(traced_out_edges))
+
+  # Copy and conjugate all reachable nodes.
+  node_dict, edge_dict = copy(old_nodes, True)
+  for t_edge in traced_out_edges:
+    # Add each edge to the copied nodes as new edge.
+    edge_dict[t_edge] = edge_dict[t_edge] ^ t_edge
+
+  return node_dict, edge_dict
+
+
 def switch_backend(nodes: Iterable[BaseNode],
                    new_backend: Text,
                    dtype: Optional[Type[np.number]] = None) -> None:


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
@@ -285,3 +285,47 @@ def test_full_graph_subgraph_dangling(backend):
   b[1] ^ c[1]
   edges = tn.get_subgraph_dangling({a, b, c})
   assert edges == {a[1], c[0]}
+
+
+def test_reduced_density(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)
+  edges = tn.get_all_edges({a, b, c})
+
+  node_dict, edge_dict = tn.reduced_density([a[0], b[1], c[2]])
+
+  assert not a[0].is_dangling()
+  assert not b[1].is_dangling()
+  assert not c[2].is_dangling()
+  assert a[1].is_dangling() & a[2].is_dangling()
+  assert b[0].is_dangling() & b[2].is_dangling()
+  assert c[0].is_dangling() & c[1].is_dangling()
+
+  for node in {a, b, c}:
+    assert node_dict[node].name == node.name
+  for edge in edges:
+    assert edge_dict[edge].name == edge.name
+
+
+def test_reduced_density_nondangling(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]
+  b[2] ^ c[1]
+
+  err_msg = "traced_out_edges must only include dangling edges!"
+  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")
+  a = tn.Node(
+      np.array([[0.0, 1.0j], [-1.0j, 0.0]], dtype=np.complex64),
+      backend=backend)
+  tn.reduced_density([a[0]])
+  result = tn.contractors.greedy(tn.reachable(a), ignore_edge_order=True)
+  np.testing.assert_allclose(result.tensor, np.eye(2))