def _flatten_trace_edges( self, edges: List[network_components.Edge], new_edge_name: Optional[Text]) -> network_components.Edge: """Flatten trace edges into single edge. Args: edges: List of trace edges to flatten new_edge_name: Optional name of the new edge created. Returns: new_edge: The new edge that represents the flattening of the given edges. """ node = edges[ 0].node1 # We are in the trace case, so this is the only node. # Flatten all of the edge's axes into a a single list. perm_back = [min(e.axis1, e.axis2) for e in edges] perm_back += [max(e.axis1, e.axis2) for e in edges] perm_front = set(range(len(node.edges))) - set(perm_back) perm_front = sorted(perm_front) perm = perm_front + perm_back new_dim = self.backend.prod( [self.backend.shape(node.tensor)[e.axis1] for e in edges]) node.reorder_axes(perm) unaffected_shape = self.backend.shape(node.tensor)[:len(perm_front)] new_shape = self.backend.concat([unaffected_shape, [new_dim, new_dim]], axis=-1) node.tensor = self.backend.reshape(node.tensor, new_shape) edge1 = network_components.Edge("TraceFront", node, len(perm_front)) edge2 = network_components.Edge("TraceBack", node, len(perm_front) + 1) node.edges = node.edges[:len(perm_front)] + [edge1, edge2] new_edge = self.connect(edge1, edge2, new_edge_name) node.axis_names = None return new_edge
def disconnect( self, edge: network_components.Edge, dangling_edge_name_1: Optional[Text] = None, dangling_edge_name_2: Optional[Text] = None ) -> List[network_components.Edge]: """Break a edge into two dangling edges. Args: edge: An edge to break. dangling_edge_name_1: Optional name to give the new dangling edge 1. dangling_edge_name_2: Optional name to give the new dangling edge 2. Returns: dangling_edge_1: A new dangling edge. dangling_edge_2: A new dangling edge. Raises: ValueError: If input edge is a dangling one. """ if edge.is_dangling(): raise ValueError( "Attempted to break a dangling edge '{}'.".format(edge)) node1 = edge.node1 node2 = edge.node2 dangling_edge_name_1 = self._new_edge_name(dangling_edge_name_1) dangling_edge_name_2 = self._new_edge_name(dangling_edge_name_2) dangling_edge_1 = network_components.Edge(dangling_edge_name_1, node1, edge.axis1) dangling_edge_2 = network_components.Edge(dangling_edge_name_2, node2, edge.axis2) node1.add_edge(dangling_edge_1, edge.axis1, True) node2.add_edge(dangling_edge_2, edge.axis2, True) self.edge_order.remove(edge) return [dangling_edge_1, dangling_edge_2]
def copy(self, conj: bool = False) -> Tuple["TensorNetwork", dict, dict]: """ Return a copy of the TensorNetwork. Args: conj: Boolean. Whether to conjugate all of the nodes in the `TensorNetwork` (useful for calculating norms and reduced density matrices). Returns: A tuple containing: TensorNetwork: A copy of the network. node_dict: A dictionary mapping the nodes of the original network to the nodes of the copy. edge_dict: A dictionary mapping the edges of the original network to the edges of the copy. """ new_net = TensorNetwork(backend=self.backend.name) #TODO: add support for copying CopyTensor if conj: node_dict = { node: new_net.add_node(self.backend.conj(node.tensor), name=node.name, axis_names=node.axis_names) for node in self.nodes_set } else: node_dict = { node: new_net.add_node(node.tensor, name=node.name, axis_names=node.axis_names) for node in self.nodes_set } edge_dict = {} for edge in self.get_all_edges(): 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 = network_components.Edge(node_dict[node1], axis1, edge.name, node_dict[node2], axis2) new_edge.set_signature(edge.signature) else: new_edge = network_components.Edge(node_dict[node1], axis1, edge.name) node_dict[node1].add_edge(new_edge, axis1) if not edge.is_dangling(): node_dict[node2].add_edge(new_edge, axis2) edge_dict[edge] = new_edge return new_net, node_dict, edge_dict
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: new_edge: A new edge created by joining the two dangling edges together. Raises: ValueError: If either edge1 or edge2 is not a dangling edge. """ for edge in [edge1, edge2]: if not edge.is_dangling(): raise ValueError( "Edge '{}' is not a dangling edge. " "This edge points to nodes: '{}' and '{}'".format( edge, edge.node1, edge.node2)) node1 = edge1.node1 node2 = edge2.node1 axis1_num = node1.get_axis_number(edge1.axis1) axis2_num = node2.get_axis_number(edge2.axis1) name = self._new_edge_name(name) new_edge = network_components.Edge(name, node1, axis1_num, node2, axis2_num) node1.add_edge(new_edge, axis1_num) node2.add_edge(new_edge, axis2_num) self.edge_order.append(new_edge) return new_edge
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. """ if edge1 is edge2: raise ValueError( "Cannot connect and edge '{}' to itself.".format(edge1)) if edge1.dimension != edge2.dimension: raise ValueError("Cannot connect edges of unequal dimension. " "Dimension of edge '{}': {}, " "Dimension of edge '{}': {}.".format( edge1, edge1.dimension, edge2, edge2.dimension)) for edge in [edge1, edge2]: if not edge.is_dangling(): raise ValueError( "Edge '{}' is not a dangling edge. " "This edge points to nodes: '{}' and '{}'".format( edge, edge.node1, edge.node2)) node1 = edge1.node1 node2 = edge2.node1 axis1_num = node1.get_axis_number(edge1.axis1) axis2_num = node2.get_axis_number(edge2.axis1) name = self._new_edge_name(name) new_edge = network_components.Edge(name, node1, axis1_num, node2, axis2_num) new_edge.set_signature(self.edge_increment) node1.add_edge(new_edge, axis1_num) node2.add_edge(new_edge, axis2_num) return new_edge
def flatten_edges( self, edges: List[network_components.Edge], new_edge_name: Optional[Text] = None) -> network_components.Edge: """Flatten edges into single edge. If two nodes have multiple edges connecting them, it may be benifitial to flatten these edges into a single edge to avoid having several unnecessary trace edges. This can speed up computation time and reduce memory cost. Warning: This will remove all axes names. Args: edges: A list of edges to flatten. new_edge_name: Optional name to give to the newly created edge. Returns: new_edge: The new flattened edge. Raises: ValueError: If edges is an empty list. ValueError: If not all of the edges connect to the same node(s). ValueError: If one of the nodes connecting to these edges does not have edge definitions for all of its axes. """ if not edges: raise ValueError("At least 1 edge must be given.") if len(edges) == 1: return edges[0] # Don't bother with reshaping. # Set equality is transitive (a=b, b=c, therefore a=c) so it is only # necessary to compare the first edge against the rest. expected_nodes = set(edges[0].get_nodes()) for edge in edges: if expected_nodes != set(edge.get_nodes()): raise ValueError( "Two edges do not share the same nodes. " "'{}'s nodes: '{}', '{}'. '{}'s nodes: '{}', '{}'".format( edges[0], edges[0].node1, edges[0].node2, edge, edge.node1, edge.node2)) if len(expected_nodes) == 1: return self._flatten_trace_edges(edges, new_edge_name) # Flatten standard or dangling edges. new_dangling_edges = [] for node in expected_nodes: # Required for dangling case. if node is None: continue perm_back = [] for edge in edges: # There will only be 1 edge since we are in the standard edge case. perm_back.append(node.edges.index(edge)) perm_front = sorted(set(range(len(node.edges))) - set(perm_back)) node.reorder_axes(perm_front + perm_back) old_tensor_shape = self.backend.shape(node.tensor) # Calculate the new axis dimension as a product of the other # axes dimensions. flattened_axis_dim = self.backend.prod( old_tensor_shape[len(perm_front):]) new_tensor_shape = self.backend.concat( [old_tensor_shape[:len(perm_front)], [flattened_axis_dim]], axis=-1) new_tensor = self.backend.reshape(node.tensor, new_tensor_shape) # Modify the node in place. Currently, this is they only method that # modifies a node's tensor. node.tensor = new_tensor # This Edge is required for the connect call later. edge = network_components.Edge(new_edge_name, node, len(perm_front)) node.edges = node.edges[:len(perm_front)] + [edge] new_dangling_edges.append(edge) # TODO: Allow renaming of the new axis. node.axis_names = None node1, node2 = tuple(expected_nodes) # Sets are returned in a random order, so this is how we deal with # dangling edges. if node1 is None or node2 is None: return new_dangling_edges[0] return self.connect(new_dangling_edges[0], new_dangling_edges[1], new_edge_name)