コード例 #1
0
    def get_node(self, site: int) -> BaseNode:
        """
    Returns the `Node` object at `site`.
    If `site==len(self) - 1` `BaseMPS.connector_matrix`
    is absorbed fromt the right-hand side into the returned 
    `Node` object.

    Args:
      site: The site for which to return the `Node`.
    Returns:
      `Node`: The node at `site`.
    """
        if site >= len(self):
            raise IndexError(
                'index `site` = {} is out of range for len(mps)= {}'.format(
                    site, len(self)))
        if site < 0:
            raise ValueError(
                'index `site` has to be larger than 0 (found `site`={}).'.
                format(site))
        if (site == len(self) - 1) and (self.connector_matrix is not None):
            self.nodes[site][2] ^ self.connector_matrix[0]
            order = [
                self.nodes[site][0], self.nodes[site][1],
                self.connector_matrix[1]
            ]
            return contract_between(self.nodes[site],
                                    self.connector_matrix,
                                    name=self.nodes[site].name,
                                    output_edge_order=order)
        return self.nodes[site]
コード例 #2
0
    def apply_one_site_gate(self, gate: Union[BaseNode, Tensor],
                            site: int) -> None:
        """
    Apply a one-site gate to an MPS. This routine will in general 
    destroy any canonical form of the state. If a canonical form is needed, 
    the user can restore it using `FiniteMPS.position`

    Args:
      gate: a one-body gate
      site: the site where the gate should be applied
      
    """
        if len(gate.shape) != 2:
            raise ValueError('rank of gate is {} but has to be 2'.format(
                len(gate.shape)))
        if site < 0 or site >= len(self):
            raise ValueError(
                'site = {} is not between 0 <= site < N={}'.format(
                    site, len(self)))
        gate_node = Node(gate, backend=self.backend)
        gate_node[1] ^ self.nodes[site][1]
        edge_order = [self.nodes[site][0], gate_node[0], self.nodes[site][2]]
        self.nodes[site] = contract_between(
            gate_node, self.nodes[site],
            name=self.nodes[site].name).reorder_edges(edge_order)
コード例 #3
0
def contract_path(path: Tuple[List[Tuple[int,
                                         int]]], nodes: Iterable[AbstractNode],
                  output_edge_order: Sequence[Edge]) -> AbstractNode:
    """Contract `nodes` using `path`.

  Args:
    path: The contraction path as returned from `path_solver`.
    nodes: A collection of connected nodes.
    output_edge_order: A list of edges. Edges of the
      final node in `nodes`
      are reordered into `output_edge_order`;
  Returns:
    Final node after full contraction.
  """
    if len(path) == 0:
        return nodes

    for a, b in path:
        new_node = contract_between(nodes[a],
                                    nodes[b],
                                    allow_outer_product=True)
        nodes.append(new_node)
        nodes = utils.multi_remove(nodes, [a, b])

    # if the final node has more than one edge,
    # output_edge_order has to be specified
    final_node = nodes[0]  # nodes were connected, we checked this
    final_node.reorder_edges(output_edge_order)
    return final_node
コード例 #4
0
def contract_path(path: Tuple[List[Tuple[int,
                                         int]]], nodes: Iterable[AbstractNode],
                  output_edge_order: Sequence[Edge]) -> AbstractNode:
    """Contract `nodes` using `path`.

  Args:
    path: The contraction path as returned from `path_solver`.
    nodes: A collection of connected nodes.
    output_edge_order: A list of edges. Edges of the
      final node in `nodes`
      are reordered into `output_edge_order`;
  Returns:
    Final node after full contraction.
  """
    edges = get_all_edges(nodes)
    for edge in edges:
        if not edge.is_disabled:  #if its disabled we already contracted it
            if edge.is_trace():
                contract_parallel(edge)

    if len(nodes) == 1:
        newnode = nodes[0].copy()
        for edge in nodes[0].edges:
            redirect_edge(edge, newnode, nodes[0])
        return newnode.reorder_edges(output_edge_order)

    if len(path) == 0:
        return nodes

    for p in path:
        if len(p) > 1:
            a, b = p
            new_node = contract_between(nodes[a],
                                        nodes[b],
                                        allow_outer_product=True)
            nodes.append(new_node)
            nodes = utils.multi_remove(nodes, [a, b])

        elif len(p) == 1:
            a = p[0]
            node = nodes.pop(a)
            new_node = contract_trace_edges(node)
            nodes.append(new_node)

    # if the final node has more than one edge,
    # output_edge_order has to be specified
    final_node = nodes[0]  # nodes were connected, we checked this
    #some contractors miss trace edges
    final_node = contract_trace_edges(final_node)
    final_node.reorder_edges(output_edge_order)
    return final_node
コード例 #5
0
ファイル: network.py プロジェクト: MaxJansen/TensorNetwork
  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
コード例 #6
0
    def apply_two_site_gate(
            self,
            gate: Tensor,
            site1: int,
            site2: int,
            max_singular_values: Optional[int] = None,
            max_truncation_err: Optional[float] = None) -> Tensor:
        """Apply a two-site gate to an MPS. This routine will in general destroy
    any canonical form of the state. If a canonical form is needed, the user
    can restore it using `FiniteMPS.position`.

    Args:
      gate: A two-body gate.
      site1: The first site where the gate acts.
      site2: The second site where the gate acts.
      max_singular_values: The maximum number of singular values to keep.
      max_truncation_err: The maximum allowed truncation error.

    Returns:
      `Tensor`: A scalar tensor containing the truncated weight of the
        truncation.
    """
        if len(gate.shape) != 4:
            raise ValueError('rank of gate is {} but has to be 4'.format(
                len(gate.shape)))
        if site1 < 0 or site1 >= len(self) - 1:
            raise ValueError(
                'site1 = {} is not between 0 <= site < N - 1 = {}'.format(
                    site1, len(self)))
        if site2 < 1 or site2 >= len(self):
            raise ValueError(
                'site2 = {} is not between 1 <= site < N = {}'.format(
                    site2, len(self)))
        if site2 <= site1:
            raise ValueError(
                'site2 = {} has to be larger than site2 = {}'.format(
                    site2, site1))
        if site2 != site1 + 1:
            raise ValueError("Found site2 ={}, site1={}. Only nearest "
                             "neighbor gates are currently"
                             "supported".format(site2, site1))

        if (max_singular_values
                or max_truncation_err) and self.center_position not in (site1,
                                                                        site2):
            raise ValueError(
                'center_position = {}, but gate is applied at sites {}, {}. '
                'Truncation should only be done if the gate '
                'is applied at the center position of the MPS'.format(
                    self.center_position, site1, site2))

        gate_node = Node(gate, backend=self.backend)
        node1 = Node(self.tensors[site1], backend=self.backend)
        node2 = Node(self.tensors[site2], backend=self.backend)
        node1[2] ^ node2[0]
        gate_node[2] ^ node1[1]
        gate_node[3] ^ node2[1]
        left_edges = [node1[0], gate_node[0]]
        right_edges = [gate_node[1], node2[2]]
        result = node1 @ node2 @ gate_node
        U, S, V, tw = split_node_full_svd(
            result,
            left_edges=left_edges,
            right_edges=right_edges,
            max_singular_values=max_singular_values,
            max_truncation_err=max_truncation_err,
            left_name=node1.name,
            right_name=node2.name)
        V.reorder_edges([S[1]] + right_edges)
        left_edges = left_edges + [S[1]]
        res = contract_between(U, S, name=U.name).reorder_edges(left_edges)
        self.tensors[site1] = res.tensor
        self.tensors[site2] = V.tensor
        return tw
コード例 #7
0
def ncon(
    tensors: Sequence[Union[network_components.BaseNode, Tensor]],
    network_structure: Sequence[Sequence],
    con_order: Optional[Sequence] = None,
    out_order: Optional[Sequence] = None,
    backend: Optional[Text] = None
) -> Union[network_components.BaseNode, Tensor]:
    r"""Contracts a list of tensors or nodes according to a tensor network 
    specification.

    The network is provided as a list of lists, one for each
    tensor, specifying labels for the edges connected to that tensor.

    If a contraction order `con_order` and an output order `out_order`
    are both provided, the edge labels can be anything.
    Otherwise (`con_order == None or out_order == None`), the edge labels 
    must be nonzero integers and edges will be contracted in ascending order.
    Negative integers denote the (dangling) indices of the output tensor,
    which will be in descending order, e.g. [-1,-2,-3,...].

    For example, matrix multiplication:

    ```python
    A = np.array([[1.0, 2.0], [3.0, 4.0]])
    B = np.array([[1.0, 1.0], [0.0, 1.0]])
    ncon([A,B], [(-1, 1), (1, -2)])
    ```

    Matrix trace:

    ```python
    A = np.array([[1.0, 2.0], [3.0, 4.0]])
    ncon([A], [(1, 1)]) # 5.0
    ```

    Note: The reason `0` is not allowed as an edge label without manually
    specifying the contraction order is to maintain compatibility with the
    [original NCON implementation](https://arxiv.org/abs/1402.0939). However,
    the use of `0` in `con_order` to denote outer products is not (currently) 
    supported in this implementation.

    Args:
      tensors: List of `Tensor`s or `BaseNode`s.
      network_structure: List of lists specifying the tensor network
        structure.
      con_order: List of edge labels specifying the contraction order.
      out_order: List of edge labels specifying the output order.
      backend: String specifying the backend to use. Defaults to 
        `tensornetwork.config.default_backend`.

    Returns:
      The result of the contraction. The result is returned as a `Node`
        if all elements of `tensors` are `BaseNode` objects, else
        it is returned as a `Tensor` object.
    """
    if backend and (backend not in backend_factory._BACKENDS):
        raise ValueError("Backend '{}' does not exist".format(backend))
    if backend is None:
        backend = config.default_backend

    are_nodes = [isinstance(t, network_components.BaseNode) for t in tensors]
    nodes = {t for t in tensors if isinstance(t, network_components.BaseNode)}
    if not all([n.backend.name == backend for n in nodes]):
        raise ValueError(
            "Some nodes have backends different from '{}'".format(backend))

    _tensors = []
    for t in tensors:
        if isinstance(t, network_components.BaseNode):
            _tensors.append(t.tensor)
        else:
            _tensors.append(t)

    nodes, con_edges, out_edges = ncon_network(_tensors,
                                               network_structure,
                                               con_order=con_order,
                                               out_order=out_order,
                                               backend=backend)

    nodes = set(nodes)  # we don't need the ordering here

    # Reverse the list so we can pop from the end: O(1).
    con_edges = con_edges[::-1]
    while con_edges:
        nodes_to_contract = con_edges[-1].get_nodes()
        edges_to_contract = network_components.get_shared_edges(
            *nodes_to_contract)

        # Eat up all parallel edges that are adjacent in the ordering.
        adjacent_parallel_edges = set()
        for edge in reversed(con_edges):
            if edge in edges_to_contract:
                adjacent_parallel_edges.add(edge)
            else:
                break
        con_edges = con_edges[:-len(adjacent_parallel_edges)]

        # In an optimal ordering, all edges connecting a given pair of nodes are
        # adjacent in con_order. If this is not the case, warn the user.
        leftovers = edges_to_contract - adjacent_parallel_edges
        if leftovers:
            warnings.warn(
                "Suboptimal ordering detected. Edges {} are not adjacent in the "
                "contraction order to edges {}, connecting nodes {}. Deviating from "
                "the specified ordering!".format(
                    list(map(str, leftovers)),
                    list(map(str, adjacent_parallel_edges)),
                    list(map(str, nodes_to_contract))))
            con_edges = [e for e in con_edges if e not in edges_to_contract]

        if set(nodes_to_contract) == nodes:
            # This contraction produces the final output, so order the edges
            # here to avoid transposes in some cases.
            contraction_output_order = out_edges
        else:
            contraction_output_order = None

        nodes = nodes - set(nodes_to_contract)
        nodes.add(
            network_components.contract_between(
                *nodes_to_contract,
                name="con({},{})".format(*nodes_to_contract),
                output_edge_order=contraction_output_order))

    # TODO: More efficient ordering of products based on out_edges
    res_node = network_components.outer_product_final_nodes(nodes, out_edges)
    if all(are_nodes):
        return res_node
    return res_node.tensor
コード例 #8
0
def base(nodes: Iterable[AbstractNode],
         algorithm: utils.Algorithm,
         output_edge_order: Optional[Sequence[Edge]] = None,
         ignore_edge_order: bool = False) -> AbstractNode:
    """Base method for all `opt_einsum` contractors.

  Args:
    nodes: A collection of connected nodes.
    algorithm: `opt_einsum` contraction method to use.
    output_edge_order: An optional list of edges. Edges of the
      final node in `nodes_set`
      are reordered into `output_edge_order`;
      if final node has more than one edge,
      `output_edge_order` must be provided.
    ignore_edge_order: An option to ignore the output edge
      order.

  Returns:
    Final node after full contraction.
  """
    nodes_set = set(nodes)
    edges = get_all_edges(nodes_set)
    #output edge order has to be determinded before any contraction
    #(edges are refreshed after contractions)

    if not ignore_edge_order:
        if output_edge_order is None:
            output_edge_order = list(get_subgraph_dangling(nodes))
            if len(output_edge_order) > 1:
                raise ValueError(
                    "The final node after contraction has more than "
                    "one remaining edge. In this case `output_edge_order` "
                    "has to be provided.")

        if set(output_edge_order) != get_subgraph_dangling(nodes):
            raise ValueError("output edges are not equal to the remaining "
                             "non-contracted edges of the final node.")

    for edge in edges:
        if not edge.is_disabled:  #if its disabled we already contracted it
            if edge.is_trace():
                nodes_set.remove(edge.node1)
                nodes_set.add(contract_parallel(edge))

    if len(nodes_set) == 1:
        # There's nothing to contract.
        if ignore_edge_order:
            return list(nodes_set)[0]
        return list(nodes_set)[0].reorder_edges(output_edge_order)

    # Then apply `opt_einsum`'s algorithm
    path, nodes = utils.get_path(nodes_set, algorithm)
    for a, b in path:
        new_node = contract_between(nodes[a],
                                    nodes[b],
                                    allow_outer_product=True)
        nodes.append(new_node)
        nodes = utils.multi_remove(nodes, [a, b])

    # if the final node has more than one edge,
    # output_edge_order has to be specified
    final_node = nodes[0]  # nodes were connected, we checked this
    if not ignore_edge_order:
        final_node.reorder_edges(output_edge_order)
    return final_node
コード例 #9
0
ファイル: finite_mps.py プロジェクト: opterzhu/TensorNetwork
    def right_envs(self, sites: Sequence[int]) -> Dict:
        """Compute right reduced density matrices for site `sites. This returns a
    dict `right_envs` mapping sites (int) to Tensors. `right_envs[site]` is the
    right-reduced density matrix to the right of site `site`.

    Args:
      sites (list of int): A list of sites of the MPS.
    Returns:
      `dict` mapping `int` to `Tensor`: The right-reduced density matrices
        at each  site in `sites`.
    """
        sites = np.array(sites)
        if len(sites) == 0:
            return {}

        n1 = np.min(sites)
        #check if all elements of `sites` are within allowed range
        if not np.all(sites < len(self)):
            raise ValueError(
                'all elements of `sites` have to be < N = {}'.format(
                    len(self)))
        if not np.all(sites >= -1):
            raise ValueError('all elements of `sites` have to be >= -1')

        # right-reduced density matrices to the right of `center_position`
        # (including center_position) are all identities
        right_sites = sites[sites >= self.center_position]
        right_envs = {}
        for site in right_sites:
            right_envs[site] = Node(self.backend.eye(
                N=self.nodes[site].shape[2], dtype=self.dtype),
                                    backend=self.backend)

        # right reduced density matrices at sites < center_position
        # have to be calculated from a network contraction
        if n1 < self.center_position:
            nodes = {}
            conj_nodes = {}
            for site in reversed(range(n1 + 1, self.center_position + 1)):
                nodes[site] = Node(self.nodes[site], backend=self.backend)
                conj_nodes[site] = conj(self.nodes[site])

            nodes[self.center_position][2] ^ conj_nodes[
                self.center_position][2]
            nodes[self.center_position][1] ^ conj_nodes[
                self.center_position][1]

            for site in reversed(range(n1 + 1, self.center_position)):
                nodes[site][2] ^ nodes[site + 1][0]
                conj_nodes[site][2] ^ conj_nodes[site + 1][0]
                nodes[site][1] ^ conj_nodes[site][1]

            edges = {site: node[0] for site, node in nodes.items()}
            conj_edges = {site: node[0] for site, node in conj_nodes.items()}

            right_env = contract_between(nodes[self.center_position],
                                         conj_nodes[self.center_position])
            if self.center_position - 1 in sites:
                right_env.reorder_edges([
                    edges[self.center_position],
                    conj_edges[self.center_position]
                ])
                right_envs[self.center_position - 1] = right_env
            for site in reversed(range(n1 + 1, self.center_position)):
                right_env = contract_between(right_env, nodes[site])
                right_env = contract_between(right_env, conj_nodes[site])
                if site - 1 in sites:
                    right_env.reorder_edges([edges[site], conj_edges[site]])
                    right_envs[site - 1] = right_env

        return right_envs
コード例 #10
0
ファイル: finite_mps.py プロジェクト: opterzhu/TensorNetwork
    def left_envs(self, sites: Sequence[int]) -> Dict:
        """Compute left reduced density matrices for site `sites`. This returns a
    dict `left_envs` mapping sites (int) to Tensors. `left_envs[site]` is the
    left-reduced density matrix to the left of site `site`.

    Args:
      sites (list of int): A list of sites of the MPS.
    Returns:
      `dict` mapping `int` to `Tensor`: The left-reduced density matrices
        at each  site in `sites`.
    """
        sites = np.array(sites)  #enable logical indexing
        if len(sites) == 0:
            return {}

        n2 = np.max(sites)

        #check if all elements of `sites` are within allowed range
        if not np.all(sites <= len(self)):
            raise ValueError(
                'all elements of `sites` have to be <= N = {}'.format(
                    len(self)))
        if not np.all(sites >= 0):
            raise ValueError('all elements of `sites` have to be positive')

        # left-reduced density matrices to the left of `center_position`
        # (including center_position) are all identities
        left_sites = sites[sites <= self.center_position]
        left_envs = {}
        for site in left_sites:
            left_envs[site] = Node(self.backend.eye(
                N=self.nodes[site].shape[0], dtype=self.dtype),
                                   backend=self.backend)

        # left reduced density matrices at sites > center_position
        # have to be calculated from a network contraction
        if n2 > self.center_position:
            nodes = {}
            conj_nodes = {}
            for site in range(self.center_position, n2):
                nodes[site] = Node(self.nodes[site], backend=self.backend)
                conj_nodes[site] = conj(self.nodes[site])

            nodes[self.center_position][0] ^ conj_nodes[
                self.center_position][0]
            nodes[self.center_position][1] ^ conj_nodes[
                self.center_position][1]

            for site in range(self.center_position + 1, n2):
                nodes[site][0] ^ nodes[site - 1][2]
                conj_nodes[site][0] ^ conj_nodes[site - 1][2]
                nodes[site][1] ^ conj_nodes[site][1]

            edges = {site: node[2] for site, node in nodes.items()}
            conj_edges = {site: node[2] for site, node in conj_nodes.items()}

            left_env = contract_between(nodes[self.center_position],
                                        conj_nodes[self.center_position])
            left_env.reorder_edges([
                edges[self.center_position], conj_edges[self.center_position]
            ])
            if self.center_position + 1 in sites:
                left_envs[self.center_position + 1] = left_env
            for site in range(self.center_position + 1, n2):
                left_env = contract_between(left_env, nodes[site])
                left_env = contract_between(left_env, conj_nodes[site])
                if site + 1 in sites:
                    left_env.reorder_edges([edges[site], conj_edges[site]])
                    left_envs[site + 1] = left_env
        return left_envs
コード例 #11
0
ファイル: mps.py プロジェクト: yetesh/TensorNetwork
    def apply_two_site_gate(
            self,
            gate: Tensor,
            site1: int,
            site2: int,
            max_singular_values: Optional[int] = None,
            max_truncation_err: Optional[float] = None) -> Tensor:
        """
    Apply a two-site gate to an MPS. This routine will in general 
    destroy any canonical form of the state. If a canonical form is needed, 
    the user can restore it using MPS.position
    Args:
      gate (Tensor): a two-body gate
      site1, site2 (int, int): the sites where the gate should be applied
      max_singular_values (int): The maximum number of singular values to keep.
      max_truncation_err (float): The maximum allowed truncation error.
    """
        if len(gate.shape) != 4:
            raise ValueError('rank of gate is {} but has to be 4'.format(
                len(gate.shape)))
        if site1 < 0 or site1 >= len(self) - 1:
            raise ValueError(
                'site1 = {} is not between 0 <= site < N - 1 = {}'.format(
                    site1, len(self)))
        if site2 < 1 or site2 >= len(self):
            raise ValueError(
                'site2 = {} is not between 1 <= site < N = {}'.format(
                    site2, len(self)))
        if site2 <= site1:
            raise ValueError(
                'site2 = {} has to be larger than site2 = {}'.format(
                    site2, site1))
        if site2 != site1 + 1:
            raise ValueError(
                'site2 ={} != site1={}. Only nearest neighbor gates are currently '
                'supported'.format(site2, site1))

        if (max_singular_values
                or max_truncation_err) and self.center_position not in (site1,
                                                                        site2):
            raise ValueError(
                'center_position = {}, but gate is applied at sites {}, {}. '
                'Truncation should only be done if the gate '
                'is applied at the center position of the MPS'.format(
                    self.center_position, site1, site2))

        gate_node = Node(gate, backend=self.backend.name)
        gate_node[2] ^ self.nodes[site1][1]
        gate_node[3] ^ self.nodes[site2][1]
        left_edges = [self.nodes[site1][0], gate_node[0]]
        right_edges = [gate_node[1], self.nodes[site2][2]]
        result = self.nodes[site1] @ self.nodes[site2] @ gate_node
        U, S, V, tw = split_node_full_svd(
            result,
            left_edges=left_edges,
            right_edges=right_edges,
            max_singular_values=max_singular_values,
            max_truncation_err=max_truncation_err,
            left_name=self.nodes[site1].name,
            right_name=self.nodes[site2].name)
        V.reorder_edges([S[1]] + right_edges)
        left_edges = left_edges + [S[1]]
        self.nodes[site1] = contract_between(
            U, S, name=U.name).reorder_edges(left_edges)
        self.nodes[site2] = V
        return tw
コード例 #12
0
def _jittable_ncon(tensors, network_structure, con_order, out_order, backend):
    """Jittable Ncon function.

  Args:
    tensors: List of tensors.
    network_structure: List of list of integers that descripes the network
      structure.
    con_order: Order of the contraction.
    out_order: Order of the final axis order.
    backend: A backend object.
  
  Returns:
    The final tensor after contraction.
  """
    nodes, con_edges, out_edges = ncon_network(tensors,
                                               network_structure,
                                               con_order=con_order,
                                               out_order=out_order,
                                               backend=backend)

    nodes = set(nodes)  # we don't need the ordering here

    # Reverse the list so we can pop from the end: O(1).
    con_edges = con_edges[::-1]
    while con_edges:
        nodes_to_contract = con_edges[-1].get_nodes()
        edges_to_contract = network_components.get_shared_edges(
            *nodes_to_contract)

        # Eat up all parallel edges that are adjacent in the ordering.
        adjacent_parallel_edges = set()
        for edge in reversed(con_edges):
            if edge in edges_to_contract:
                adjacent_parallel_edges.add(edge)
            else:
                break
        con_edges = con_edges[:-len(adjacent_parallel_edges)]

        # In an optimal ordering, all edges connecting a given pair of nodes are
        # adjacent in con_order. If this is not the case, warn the user.
        leftovers = edges_to_contract - adjacent_parallel_edges
        if leftovers:
            warnings.warn(
                "Suboptimal ordering detected. Edges {} are not adjacent in the "
                "contraction order to edges {}, connecting nodes {}. Deviating from "
                "the specified ordering!".format(
                    list(map(str, leftovers)),
                    list(map(str, adjacent_parallel_edges)),
                    list(map(str, nodes_to_contract))))
            con_edges = [e for e in con_edges if e not in edges_to_contract]

        if set(nodes_to_contract) == nodes:
            # This contraction produces the final output, so order the edges
            # here to avoid transposes in some cases.
            contraction_output_order = out_edges
        else:
            contraction_output_order = None

        nodes = nodes - set(nodes_to_contract)
        nodes.add(
            network_components.contract_between(
                *nodes_to_contract,
                name="con({},{})".format(*nodes_to_contract),
                output_edge_order=contraction_output_order))
    # TODO: More efficient ordering of products based on out_edges
    res_node = network_components.outer_product_final_nodes(nodes, out_edges)
    return res_node.tensor