def _build_network( tensors: Sequence[Tensor], network_structure: Sequence[Sequence], backend: Text ) -> Tuple[List[network_components.BaseNode], Dict[Any, network_components.Edge]]: nodes = [] edges = {} for i, (tensor, edge_lbls) in enumerate(zip(tensors, network_structure)): if len(tensor.shape) != len(edge_lbls): raise ValueError( "Incorrect number of edge labels specified tensor {}".format( i)) if isinstance(tensor, network_components.BaseNode): node = tensor else: node = network_components.Node(tensor, name="tensor_{}".format(i), backend=backend) nodes.append(node) for (axis_num, edge_lbl) in enumerate(edge_lbls): if edge_lbl not in edges: e = node[axis_num] e.set_name(str(edge_lbl)) edges[edge_lbl] = e else: # This will raise an error if the edges are not dangling. e = network_components.connect(edges[edge_lbl], node[axis_num], name=str(edge_lbl)) edges[edge_lbl] = e return nodes, edges
def test_cnot_gate(): # Prepare input state: |11> q0_in = Node(np.array([0, 1], dtype=np.float64)) q1_in = Node(np.array([0, 1], dtype=np.float64)) # Prepare output state: |10> q0_out = Node(np.array([0, 1], dtype=np.float64)) q1_out = Node(np.array([1, 0], dtype=np.float64)) # Build quantum circuit copy_node, q0_t1, q1_t1 = add_cnot(q0_in[0], q1_in[0]) network_components.connect(q0_t1, q0_out[0]) network_components.connect(q1_t1, q1_out[0]) # Contract the network, first using Bucket Elimination, then once # no more copy tensors are left to exploit, fall back to the naive # contractor. contraction_order = (copy_node,) net = bucket([q0_in, q1_in, q0_out, q1_out, copy_node], contraction_order) result = greedy(net) # Verify that CNOT has turned |11> into |10>. np.testing.assert_allclose(result.get_tensor(), 1.0)
def test_swap_gate(): # Prepare input state: 0.6|00> + 0.8|10> q0_in = Node(np.array([0.6, 0.8], dtype=np.float64), backend="jax") q1_in = Node(np.array([1, 0], dtype=np.float64), backend="jax") # Prepare output state: 0.6|00> + 0.8|01> q0_out = Node(np.array([1, 0], dtype=np.float64), backend="jax") q1_out = Node(np.array([0.6, 0.8], dtype=np.float64), backend="jax") # Build quantum circuit: three CNOTs implement a SWAP copy_node_1, q0_t1, q1_t1 = add_cnot(q0_in[0], q1_in[0], backend="jax") copy_node_2, q1_t2, q0_t2 = add_cnot(q1_t1, q0_t1, backend="jax") copy_node_3, q0_t3, q1_t3 = add_cnot(q0_t2, q1_t2, backend="jax") network_components.connect(q0_t3, q0_out[0]) network_components.connect(q1_t3, q1_out[0]) # Contract the network, first Bucket Elimination, then greedy to complete. contraction_order = (copy_node_1, copy_node_2, copy_node_3) nodes = [q0_in, q0_out, q1_in, q1_out, copy_node_1, copy_node_2, copy_node_3] net = bucket(nodes, contraction_order) result = greedy(net) # Verify that SWAP has turned |10> into |01> and kept |00> unchanged. np.testing.assert_allclose(result.get_tensor(), 1.0)
def add_cnot(q0: network_components.Edge, q1: network_components.Edge, backend: str = "numpy" ) -> Tuple[network_components.CopyNode, network_components.Edge, network_components.Edge]: """Adds the CNOT quantum gate to tensor network. CNOT consists of two rank-3 tensors: a COPY tensor on the control qubit and a XOR tensor on the target qubit. Args: q0: Input edge for the control qubit. q1: Input edge for the target qubit. backend: backend to use Returns: Tuple with three elements: - copy tensor corresponding to the control qubit - output edge for the control qubit and - output edge for the target qubit. """ control = CopyNode(rank=3, dimension=2, backend=backend) xor = np.array([[[1, 0], [0, 1]], [[0, 1], [1, 0]]], dtype=np.float64) target = Node(xor, backend=backend) network_components.connect(q0, control[0]) network_components.connect(q1, target[0]) network_components.connect(control[1], target[1]) return (control, control[2], target[2])
def connect(self, edge1: network_components.Edge, edge2: network_components.Edge, name: Optional[Text] = None) -> network_components.Edge: """Join two dangling edges into a new edge. Args: edge1: The first dangling edge. edge2: The second dangling edge. name: Optional name to give the new edge. Returns: A new edge created by joining the two dangling edges together. Raises: ValueError: If either edge1 or edge2 is not a dangling edge or if edge1 and edge2 are the same edge. """ name = self._new_edge_name(name) new_edge = network_components.connect(edge1, edge2, name) new_edge.set_signature(self.edge_increment) return new_edge
def eliminate_identities(nodes: Collection[BaseNode]) -> Tuple[dict, dict]: """Eliminates any connected CopyNodes that are identity matrices. This will modify the network represented by `nodes`. Only identities that are connected to other nodes are eliminated. Args: nodes: Collection of nodes to search. Returns: nodes_dict: Dictionary mapping remaining Nodes to any replacements. dangling_edges_dict: Dictionary specifying all dangling-edge replacements. """ nodes_dict = {} dangling_edges_dict = {} for n in nodes: if isinstance(n, CopyNode) and n.get_rank() == 2 and not ( n[0].is_dangling() and n[1].is_dangling()): old_edges = [n[0], n[1]] _, new_edges = remove_node(n) if 0 in new_edges and 1 in new_edges: e = connect(new_edges[0], new_edges[1]) elif 0 in new_edges: # 1 was dangling dangling_edges_dict[old_edges[1]] = new_edges[0] elif 1 in new_edges: # 0 was dangling dangling_edges_dict[old_edges[0]] = new_edges[1] else: # Trace of identity, so replace with a scalar node! d = n.get_dimension(0) # NOTE: Assume CopyNodes have numpy dtypes. nodes_dict[n] = Node(np.array(d, dtype=n.dtype), backend=n.backend) else: for e in n.get_all_dangling(): dangling_edges_dict[e] = e nodes_dict[n] = n return nodes_dict, dangling_edges_dict
def split_node_full_svd( node: BaseNode, left_edges: List[Edge], right_edges: List[Edge], max_singular_values: Optional[int] = None, max_truncation_err: Optional[float] = None, left_name: Optional[Text] = None, middle_name: Optional[Text] = None, right_name: Optional[Text] = None, left_edge_name: Optional[Text] = None, right_edge_name: Optional[Text] = None, ) -> Tuple[BaseNode, BaseNode, BaseNode, Tensor]: """Split a node by doing a full singular value decomposition. Let M be the matrix created by flattening left_edges and right_edges into 2 axes. Let :math:`U S V^* = M` be the Singular Value Decomposition of :math:`M`. The left most node will be :math:`U` tensor of the SVD, the middle node is the diagonal matrix of the singular values, ordered largest to smallest, and the right most node will be the :math:`V*` tensor of the SVD. The singular value decomposition is truncated if `max_singular_values` or `max_truncation_err` is not `None`. The truncation error is the 2-norm of the vector of truncated singular values. If only `max_truncation_err` is set, as many singular values will be truncated as possible while maintaining: `norm(truncated_singular_values) <= max_truncation_err`. If only `max_singular_values` is set, the number of singular values kept will be `min(max_singular_values, number_of_singular_values)`, so that `max(0, number_of_singular_values - max_singular_values)` are truncated. If both `max_truncation_err` and `max_singular_values` are set, `max_singular_values` takes priority: The truncation error may be larger than `max_truncation_err` if required to satisfy `max_singular_values`. Args: node: The node you want to split. left_edges: The edges you want connected to the new left node. right_edges: The edges you want connected to the new right node. max_singular_values: The maximum number of singular values to keep. max_truncation_err: The maximum allowed truncation error. left_name: The name of the new left node. If None, a name will be generated automatically. middle_name: The name of the new center node. If None, a name will be generated automatically. right_name: The name of the new right node. If None, a name will be generated automatically. left_edge_name: The name of the new left `Edge` connecting the new left node (`U`) and the new central node (`S`). If `None`, a name will be generated automatically. right_edge_name: The name of the new right `Edge` connecting the new central node (`S`) and the new right node (`V*`). If `None`, a name will be generated automatically. Returns: A tuple containing: left_node: A new node created that connects to all of the `left_edges`. Its underlying tensor is :math:`U` singular_values_node: A new node that has 2 edges connecting `left_node` and `right_node`. Its underlying tensor is :math:`S` right_node: A new node created that connects to all of the `right_edges`. Its underlying tensor is :math:`V^*` truncated_singular_values: The vector of truncated singular values. """ if not hasattr(node, 'backend'): raise TypeError('Node {} of type {} has no `backend`'.format( node, type(node))) if node.axis_names and left_edge_name and right_edge_name: left_axis_names = [] right_axis_names = [right_edge_name] for edge in left_edges: left_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) for edge in right_edges: right_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) left_axis_names.append(left_edge_name) center_axis_names = [left_edge_name, right_edge_name] else: left_axis_names = None center_axis_names = None right_axis_names = None backend = node.backend node.reorder_edges(left_edges + right_edges) u, s, vh, trun_vals = backend.svd_decomposition(node.tensor, len(left_edges), max_singular_values, max_truncation_err) left_node = Node(u, name=left_name, axis_names=left_axis_names, backend=backend.name) singular_values_node = Node(backend.diag(s), name=middle_name, axis_names=center_axis_names, backend=backend.name) right_node = Node(vh, name=right_name, axis_names=right_axis_names, backend=backend.name) for i, edge in enumerate(left_edges): left_node.add_edge(edge, i) edge.update_axis(i, node, i, left_node) 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) connect(left_node.edges[-1], singular_values_node.edges[0], name=left_edge_name) connect(singular_values_node.edges[1], right_node.edges[0], name=right_edge_name) return left_node, singular_values_node, right_node, trun_vals
def split_node_rq( node: BaseNode, left_edges: List[Edge], right_edges: List[Edge], left_name: Optional[Text] = None, right_name: Optional[Text] = None, edge_name: Optional[Text] = None, ) -> Tuple[BaseNode, BaseNode]: """Split a `Node` using RQ (reversed QR) decomposition Let M be the matrix created by flattening left_edges and right_edges into 2 axes. Let :math:`QR = M^*` be the QR Decomposition of :math:`M^*`. This will split the network into 2 nodes. The left node's tensor will be :math:`R^*` (a lower triangular matrix) and the right node's tensor will be :math:`Q^*` (an orthonormal matrix) Args: node: The node you want to split. left_edges: The edges you want connected to the new left node. right_edges: The edges you want connected to the new right node. left_name: The name of the new left node. If `None`, a name will be generated automatically. right_name: The name of the new right node. If `None`, a name will be generated automatically. edge_name: The name of the new `Edge` connecting the new left and right node. If `None`, a name will be generated automatically. Returns: A tuple containing: left_node: A new node created that connects to all of the `left_edges`. Its underlying tensor is :math:`Q` right_node: A new node created that connects to all of the `right_edges`. Its underlying tensor is :math:`R` """ if not hasattr(node, 'backend'): raise TypeError('Node {} of type {} has no `backend`'.format( node, type(node))) if node.axis_names and edge_name: left_axis_names = [] right_axis_names = [edge_name] for edge in left_edges: left_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) for edge in right_edges: right_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) left_axis_names.append(edge_name) else: 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)) left_node = Node(r, name=left_name, axis_names=left_axis_names, backend=backend.name) for i, edge in enumerate(left_edges): left_node.add_edge(edge, i) edge.update_axis(i, node, i, left_node) right_node = Node(q, name=right_name, axis_names=right_axis_names, backend=backend.name) 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) connect(left_node.edges[-1], right_node.edges[0], name=edge_name) return left_node, right_node
def split_node( node: BaseNode, left_edges: List[Edge], right_edges: List[Edge], max_singular_values: Optional[int] = None, max_truncation_err: Optional[float] = None, left_name: Optional[Text] = None, right_name: Optional[Text] = None, edge_name: Optional[Text] = None, ) -> Tuple[BaseNode, BaseNode, Tensor]: """Split a `Node` using Singular Value Decomposition. Let M be the matrix created by flattening left_edges and right_edges into 2 axes. Let :math:`U S V^* = M` be the Singular Value Decomposition of :math:`M`. This will split the network into 2 nodes. The left node's tensor will be :math:`U \\sqrt{S}` and the right node's tensor will be :math:`\\sqrt{S} V^*` where :math:`V^*` is the adjoint of :math:`V`. The singular value decomposition is truncated if `max_singular_values` or `max_truncation_err` is not `None`. The truncation error is the 2-norm of the vector of truncated singular values. If only `max_truncation_err` is set, as many singular values will be truncated as possible while maintaining: `norm(truncated_singular_values) <= max_truncation_err`. If only `max_singular_values` is set, the number of singular values kept will be `min(max_singular_values, number_of_singular_values)`, so that `max(0, number_of_singular_values - max_singular_values)` are truncated. If both `max_truncation_err` and `max_singular_values` are set, `max_singular_values` takes priority: The truncation error may be larger than `max_truncation_err` if required to satisfy `max_singular_values`. Args: node: The node you want to split. left_edges: The edges you want connected to the new left node. right_edges: The edges you want connected to the new right node. max_singular_values: The maximum number of singular values to keep. max_truncation_err: The maximum allowed truncation error. left_name: The name of the new left node. If `None`, a name will be generated automatically. right_name: The name of the new right node. If `None`, a name will be genenerated automatically. edge_name: The name of the new `Edge` connecting the new left and right node. If `None`, a name will be generated automatically. The new axis will get the same name as the edge. Returns: A tuple containing: left_node: A new node created that connects to all of the `left_edges`. Its underlying tensor is :math:`U \\sqrt{S}` right_node: A new node created that connects to all of the `right_edges`. Its underlying tensor is :math:`\\sqrt{S} V^*` truncated_singular_values: The vector of truncated singular values. """ if not hasattr(node, 'backend'): raise TypeError('Node {} of type {} has no `backend`'.format( node, type(node))) if node.axis_names and edge_name: left_axis_names = [] right_axis_names = [edge_name] for edge in left_edges: left_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) for edge in right_edges: right_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) left_axis_names.append(edge_name) else: left_axis_names = None right_axis_names = None backend = node.backend node.reorder_edges(left_edges + right_edges) u, s, vh, trun_vals = backend.svd_decomposition(node.tensor, len(left_edges), max_singular_values, max_truncation_err) sqrt_s = backend.sqrt(s) u_s = u * sqrt_s # We have to do this since we are doing element-wise multiplication against # the first axis of vh. If we don't, it's possible one of the other axes of # vh will be the same size as sqrt_s and would multiply across that axis # instead, which is bad. sqrt_s_broadcast_shape = backend.concat( [backend.shape(sqrt_s), [1] * (len(vh.shape) - 1)], axis=-1) vh_s = vh * backend.reshape(sqrt_s, sqrt_s_broadcast_shape) left_node = Node(u_s, name=left_name, axis_names=left_axis_names, backend=backend.name) for i, edge in enumerate(left_edges): left_node.add_edge(edge, i) edge.update_axis(i, node, i, left_node) right_node = Node(vh_s, name=right_name, axis_names=right_axis_names, backend=backend.name) 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) 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
def split_node_qr( node: BaseNode, left_edges: List[Edge], right_edges: List[Edge], left_name: Optional[Text] = None, right_name: Optional[Text] = None, edge_name: Optional[Text] = None, ) -> Tuple[BaseNode, BaseNode]: """Split a `node` using QR decomposition. Let :math:`M` be the matrix created by flattening `left_edges` and `right_edges` into 2 axes. Let :math:`QR = M` be the QR Decomposition of :math:`M`. This will split the network into 2 nodes. The `left node`'s tensor will be :math:`Q` (an orthonormal matrix) and the `right node`'s tensor will be :math:`R` (an upper triangular matrix) Args: node: The node you want to split. left_edges: The edges you want connected to the new left node. right_edges: The edges you want connected to the new right node. left_name: The name of the new left node. If `None`, a name will be generated automatically. right_name: The name of the new right node. If `None`, a name will be generated automatically. edge_name: The name of the new `Edge` connecting the new left and right node. If `None`, a name will be generated automatically. Returns: A tuple containing: left_node: A new node created that connects to all of the `left_edges`. Its underlying tensor is :math:`Q` right_node: A new node created that connects to all of the `right_edges`. Its underlying tensor is :math:`R` Raises: AttributeError: If `node` has no backend attribute """ if not hasattr(node, 'backend'): raise AttributeError('Node {} of type {} has no `backend`'.format( node, type(node))) if node.axis_names and edge_name: left_axis_names = [] right_axis_names = [edge_name] for edge in left_edges: left_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) for edge in right_edges: right_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) left_axis_names.append(edge_name) else: left_axis_names = None right_axis_names = None backend = node.backend 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(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(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
def split_node( node: BaseNode, left_edges: List[Edge], right_edges: List[Edge], max_singular_values: Optional[int] = None, max_truncation_err: Optional[float] = None, relative: Optional[bool] = False, left_name: Optional[Text] = None, right_name: Optional[Text] = None, edge_name: Optional[Text] = None, ) -> Tuple[BaseNode, BaseNode, Tensor]: """Split a `node` using Singular Value Decomposition. Let :math:`M` be the matrix created by flattening `left_edges` and `right_edges` into 2 axes. Let :math:`U S V^* = M` be the SVD of :math:`M`. This will split the network into 2 nodes. The left node's tensor will be :math:`U \\sqrt{S}` and the right node's tensor will be :math:`\\sqrt{S} V^*` where :math:`V^*` is the adjoint of :math:`V`. The singular value decomposition is truncated if `max_singular_values` or `max_truncation_err` is not `None`. The truncation error is the 2-norm of the vector of truncated singular values. If only `max_truncation_err` is set, as many singular values will be truncated as possible while maintaining: `norm(truncated_singular_values) <= max_truncation_err`. If `relative` is set `True` then `max_truncation_err` is understood relative to the largest singular value. If only `max_singular_values` is set, the number of singular values kept will be `min(max_singular_values, number_of_singular_values)`, so that `max(0, number_of_singular_values - max_singular_values)` are truncated. If both `max_truncation_err` and `max_singular_values` are set, `max_singular_values` takes priority: The truncation error may be larger than `max_truncation_err` if required to satisfy `max_singular_values`. Args: node: The node you want to split. left_edges: The edges you want connected to the new left node. right_edges: The edges you want connected to the new right node. max_singular_values: The maximum number of singular values to keep. max_truncation_err: The maximum allowed truncation error. relative: Multiply `max_truncation_err` with the largest singular value. left_name: The name of the new left node. If `None`, a name will be generated automatically. right_name: The name of the new right node. If `None`, a name will be generated automatically. edge_name: The name of the new `Edge` connecting the new left and right node. If `None`, a name will be generated automatically. The new axis will get the same name as the edge. Returns: A tuple containing: left_node: A new node created that connects to all of the `left_edges`. Its underlying tensor is :math:`U \\sqrt{S}` right_node: A new node created that connects to all of the `right_edges`. Its underlying tensor is :math:`\\sqrt{S} V^*` truncated_singular_values: The vector of truncated singular values. Raises: AttributeError: If `node` has no backend attribute """ if not hasattr(node, 'backend'): raise AttributeError('Node {} of type {} has no `backend`'.format( node, type(node))) if node.axis_names and edge_name: left_axis_names = [] right_axis_names = [edge_name] for edge in left_edges: left_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) for edge in right_edges: right_axis_names.append(node.axis_names[edge.axis1] if edge.node1 is node else node.axis_names[edge.axis2]) left_axis_names.append(edge_name) else: left_axis_names = None right_axis_names = None backend = node.backend transp_tensor = node.tensor_from_edge_order(left_edges + right_edges) u, s, vh, trun_vals = backend.svd_decomposition( transp_tensor, len(left_edges), max_singular_values, max_truncation_err, relative=relative) sqrt_s = backend.sqrt(s) u_s = backend.broadcast_right_multiplication(u, sqrt_s) vh_s = backend.broadcast_left_multiplication(sqrt_s, vh) 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(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(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