示例#1
0
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
示例#2
0
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)
示例#3
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)
示例#4
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])
示例#5
0
    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
示例#6
0
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
示例#10
0
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
示例#11
0
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