def remove_node( self, node: network_components.BaseNode ) -> Tuple[Dict[Text, network_components.Edge], Dict[ int, network_components.Edge]]: """Remove a node from the network. Args: node: The node to be removed. Returns: broken_edges_by_name: A Dictionary mapping `node`'s axis names to the newly broken edges. broken_edges_by_axis: A Dictionary mapping `node`'s axis numbers to the newly broken edges. Raises: ValueError: If the node isn't in the network. """ if node not in self: raise ValueError("Node '{}' is not in the network.".format(node)) broken_edges_by_name = {} broken_edges_by_axis = {} print(len(node.axis_names)) for i, name in enumerate(node.axis_names): print(i, name) if not node[i].is_dangling() and not node[i].is_trace(): edge1, edge2 = self.disconnect(node[i]) new_broken_edge = edge1 if edge1.node1 is not node else edge2 broken_edges_by_axis[i] = new_broken_edge broken_edges_by_name[name] = new_broken_edge self.nodes_set.remove(node) node.disable() return broken_edges_by_name, broken_edges_by_axis
def _remove_edges(self, edges: Set[network_components.Edge], node1: network_components.BaseNode, node2: network_components.BaseNode, new_node: network_components.BaseNode) -> None: """Collapse a list of edges shared by two nodes in the network. Collapses the edges and updates the rest of the network. The nodes that currently share the edges in `edges` must be supplied as `node1` and `node2`. The ordering of `node1` and `node2` must match the axis ordering of `new_node` (as determined by the contraction procedure). Args: edges: The edges to contract. node1: The old node that supplies the first edges of `new_node`. node2: The old node that supplies the last edges of `new_node`. new_node: The new node that represents the contraction of the two old nodes. Raises: Value Error: If edge isn't in the network. """ network_components._remove_edges(edges, node1, node2, new_node) if node1 in self.nodes_set: self.nodes_set.remove(node1) if node2 in self.nodes_set: self.nodes_set.remove(node2) if not node1.is_disabled: node1.disable() if not node1.is_disabled: node2.disable()
def outer_product(self, node1: network_components.BaseNode, node2: network_components.BaseNode, name: Optional[Text] = None) -> network_components.BaseNode: """Calculates an outer product of the two nodes. This causes the nodes to combine their edges and axes, so the shapes are combined. For example, if `a` had a shape (2, 3) and `b` had a shape (4, 5, 6), then the node `net.outer_product(a, b) will have shape (2, 3, 4, 5, 6). Args: node1: The first node. The axes on this node will be on the left side of the new node. node2: The second node. The axes on this node will be on the right side of the new node. name: Optional name to give the new node created. Returns: A new node. Its shape will be node1.shape + node2.shape """ new_node = self.add_node( network_components.outer_product(node1, node2, name, axis_names=None)) # Remove the nodes from the set. if node1 in self.nodes_set: self.nodes_set.remove(node1) if node2 in self.nodes_set: self.nodes_set.remove(node2) if not node1.is_disabled: node1.disable() if not node2.is_disabled: node2.disable() return new_node
def split_node_rq( self, node: network_components.BaseNode, left_edges: List[network_components.Edge], right_edges: List[network_components.Edge], left_name: Optional[Text] = None, right_name: Optional[Text] = None, ) -> Tuple[network_components.BaseNode, network_components.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. 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` """ node.reorder_edges(left_edges + right_edges) q, r = self.backend.qr_decomposition(node.tensor, len(left_edges)) left_node = self.add_node(q, name=left_name) for i, edge in enumerate(left_edges): left_node.add_edge(edge, i) edge.update_axis(i, node, i, left_node) right_node = self.add_node(r, name=right_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) self.connect(left_node[-1], right_node[0]) self.nodes_set.remove(node) return left_node, right_node
def transpose(node: BaseNode, permutation: Sequence[Union[Text, int]], name: Optional[Text] = None, axis_names: Optional[List[Text]] = None) -> BaseNode: """Transpose `node` Args: node: A `BaseNode`. permutation: A list of int ro str. The permutation of the axis name: Optional name to give the new node. axis_names: Optional list of names for the axis. Returns: A new node. The transpose of `node`. Raises: TypeError: If `node` has no `backend` attribute. ValueError: If either `permutation` is not the same as expected or if you try to permute with a trace edge. AttributeError: If `node` has no tensor. """ if not hasattr(node, 'backend'): raise TypeError('Node {} of type {} has no `backend`'.format( node, type(node))) perm = [node.get_axis_number(p) for p in permutation] if not axis_names: axis_names = node.axis_names new_node = Node(node.tensor, name=name, axis_names=node.axis_names, backend=node.backend.name) return new_node.reorder_axes(perm)
def contract_between( self, node1: network_components.BaseNode, node2: network_components.BaseNode, name: Optional[Text] = None, allow_outer_product: bool = False, output_edge_order: Optional[Sequence[network_components.Edge]] = None, ) -> network_components.BaseNode: """Contract all of the edges between the two given nodes. Args: node1: The first node. node2: The second node. name: Name to give to the new node created. allow_outer_product: Optional boolean. If two nodes do not share any edges and `allow_outer_product` is set to `True`, then we return the outer product of the two nodes. Else, we raise a `ValueError`. output_edge_order: Optional sequence of Edges. When not `None`, must contain all edges belonging to, but not shared by `node1` and `node2`. The axes of the new node will be permuted (if necessary) to match this ordering of Edges. Returns: The new node created. Raises: ValueError: If no edges are found between node1 and node2 and `allow_outer_product` is set to `False`. """ new_node = self.add_node( network_components.contract_between( node1, node2, name, allow_outer_product, output_edge_order, axis_names=None)) if node1 in self.nodes_set: self.nodes_set.remove(node1) if node2 in self.nodes_set: self.nodes_set.remove(node2) if not node1.is_disabled: node1.disable() if not node2.is_disabled: node2.disable() return new_node
def split_node_rq( self, node: network_components.BaseNode, left_edges: List[network_components.Edge], right_edges: List[network_components.Edge], left_name: Optional[Text] = None, right_name: Optional[Text] = None, edge_name: Optional[Text] = None, ) -> Tuple[network_components.BaseNode, network_components.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` """ r, q = network_operations.split_node_rq(node, left_edges, right_edges, left_name, right_name, edge_name) left_node = self.add_node(r) right_node = self.add_node(q) self.nodes_set.remove(node) node.disable() return left_node, right_node
def _remove_trace_edge(self, edge: network_components.Edge, new_node: network_components.BaseNode) -> None: """Collapse a trace edge. Collapses a trace edge and updates the network. Args: edge: The edge to contract. new_node: The new node created after contraction. Returns: The node that had the contracted edge. Raises: ValueError: If edge is not a trace edge. """ if edge.is_dangling(): raise ValueError("Attempted to remove dangling edge '{}'.".format(edge)) if edge.node1 is not edge.node2: raise ValueError("Edge '{}' is not a trace edge.".format(edge)) axes = sorted([edge.axis1, edge.axis2]) node_edges = edge.node1.edges[:] node_edges.pop(axes[0]) node_edges.pop(axes[1] - 1) seen_edges = set() for tmp_edge in node_edges: if tmp_edge in seen_edges: continue else: seen_edges.add(tmp_edge) if tmp_edge.node1 is edge.node1: to_reduce = 0 to_reduce += 1 if tmp_edge.axis1 > axes[0] else 0 to_reduce += 1 if tmp_edge.axis1 > axes[1] else 0 tmp_edge.axis1 -= to_reduce tmp_edge.node1 = new_node if tmp_edge.node2 is edge.node1: to_reduce = 0 to_reduce += 1 if tmp_edge.axis2 > axes[0] else 0 to_reduce += 1 if tmp_edge.axis2 > axes[1] else 0 tmp_edge.axis2 -= to_reduce tmp_edge.node2 = new_node # Update edges for the new node. for i, e in enumerate(node_edges): new_node.add_edge(e, i) self.nodes_set.remove(edge.node1)
def split_node( self, node: network_components.BaseNode, left_edges: List[network_components.Edge], right_edges: List[network_components.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[network_components.BaseNode, network_components.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 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:`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. """ left, right, trun_vals = network_operations.split_node( node, left_edges, right_edges, max_singular_values, max_truncation_err, left_name, right_name, edge_name) left_node = self.add_node(left) right_node = self.add_node(right) self.nodes_set.remove(node) node.disable() return left_node, right_node, trun_vals
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
def split_node_full_svd( self, node: network_components.BaseNode, left_edges: List[network_components.Edge], right_edges: List[network_components.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[network_components.BaseNode, network_components.BaseNode, network_components.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. """ U, S, V, trun_vals = network_operations.split_node_full_svd( node, left_edges, right_edges, max_singular_values, max_truncation_err, left_name, middle_name, right_name, left_edge_name, right_edge_name) left_node = self.add_node(U) singular_values_node = self.add_node(S) right_node = self.add_node(V) self.nodes_set.remove(node) node.disable() return left_node, singular_values_node, right_node, trun_vals
def split_node_full_svd(self, node: network_components.BaseNode, left_edges: List[network_components.Edge], right_edges: List[network_components.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 ) -> Tuple[network_components.BaseNode, network_components.BaseNode, network_components.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. 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. """ node.reorder_edges(left_edges + right_edges) u, s, vh, trun_vals = self.backend.svd_decomposition( node.tensor, len(left_edges), max_singular_values, max_truncation_err) left_node = self.add_node(u, name=left_name) singular_values_node = self.add_node(self.backend.diag(s), name=middle_name) right_node = self.add_node(vh, name=right_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) self.connect(left_node[-1], singular_values_node[0]) self.connect(singular_values_node[1], right_node[0]) self.nodes_set.remove(node) return left_node, singular_values_node, right_node, trun_vals
def _remove_edges(self, edges: Set[network_components.Edge], node1: network_components.BaseNode, node2: network_components.BaseNode, new_node: network_components.BaseNode) -> None: """Collapse a list of edges shared by two nodes in the network. Collapses the edges and updates the rest of the network. The nodes that currently share the edges in `edges` must be supplied as `node1` and `node2`. The ordering of `node1` and `node2` must match the axis ordering of `new_node` (as determined by the contraction procedure). Args: edges: The edges to contract. node1: The old node that supplies the first edges of `new_node`. node2: The old node that supplies the last edges of `new_node`. new_node: The new node that represents the contraction of the two old nodes. Raises: Value Error: If edge isn't in the network. """ if node1 is node2: raise ValueError( "node1 and node2 are the same ('{}' == '{}'), but trace edges cannot " "be removed by _remove_edges.".format(node1, node2)) node1_edges = node1.edges[:] node2_edges = node2.edges[:] nodes_set = set([node1, node2]) for edge in edges: if edge.is_dangling(): raise ValueError("Attempted to remove dangling edge '{}'.".format(edge)) if set([edge.node1, edge.node2]) != nodes_set: raise ValueError( "Attempted to remove edges belonging to different node pairs: " "'{}' != '{}'.".format(nodes_set, set([edge.node1, edge.node2]))) remaining_edges = [] for (i, edge) in enumerate(node1_edges): if edge not in edges: # NOTE: Makes the cost quadratic in # edges edge.update_axis( old_node=node1, old_axis=i, new_axis=len(remaining_edges), new_node=new_node) remaining_edges.append(edge) for (i, edge) in enumerate(node2_edges): if edge not in edges: edge.update_axis( old_node=node2, old_axis=i, new_axis=len(remaining_edges), new_node=new_node) remaining_edges.append(edge) for (i, edge) in enumerate(remaining_edges): new_node.add_edge(edge, i) # Remove nodes self.nodes_set.remove(node1) self.nodes_set.remove(node2)