diff --git a/pytket/extensions/cutensornet/structured_state/mps_gate.py b/pytket/extensions/cutensornet/structured_state/mps_gate.py index af66f432..8c57f868 100644 --- a/pytket/extensions/cutensornet/structured_state/mps_gate.py +++ b/pytket/extensions/cutensornet/structured_state/mps_gate.py @@ -118,6 +118,7 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: # r -> physical bond of the right tensor in the MPS # L -> left bond of the outcome of the gate # R -> right bond of the outcome of the gate + # S -> shared bond of the gate tensor's SVD # a,b,c -> the virtual bonds of the tensors if l_pos == positions[0]: @@ -125,19 +126,27 @@ def _apply_2q_gate(self, positions: tuple[int, int], gate: Op) -> MPSxGate: else: # Implicit swap gate_bonds = "RLrl" - left_bonds = "abl" - right_bonds = "bcr" - result_bonds = "acLR" + # Apply SVD on the gate tensor to remove any zero singular values ASAP + svd_method = tensor.SVDMethod( + abs_cutoff=self._cfg.zero, + partition="U", # Contract S directly into U + ) + # Apply the SVD decomposition using the configuration defined above + U, S, V = tensor.decompose( + f"{gate_bonds}->SLl,SRr", gate_tensor, method=svd_method, options=options + ) + assert S is None # Due to "partition" option in SVDMethod # Contract self._logger.debug("Contracting the two-qubit gate with its site tensors...") T = cq.contract( - gate_bonds + "," + left_bonds + "," + right_bonds + "->" + result_bonds, - gate_tensor, + f"SLl,abl,SRr,bcr->acLR", + U, self.tensors[l_pos], + V, self.tensors[r_pos], options=options, - optimize={"path": [(0, 1), (0, 1)]}, + optimize={"path": [(0, 1), (0, 1), (0, 1)]}, ) self._logger.debug(f"Intermediate tensor of size (MiB)={T.nbytes / 2**20}") diff --git a/pytket/extensions/cutensornet/structured_state/ttn_gate.py b/pytket/extensions/cutensornet/structured_state/ttn_gate.py index 5c7d2c85..d5183939 100644 --- a/pytket/extensions/cutensornet/structured_state/ttn_gate.py +++ b/pytket/extensions/cutensornet/structured_state/ttn_gate.py @@ -115,6 +115,7 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: # b -> the input bond of the gate on q1 # A -> the output bond of the gate on q0 # B -> the output bond of the gate on q1 + # S -> the shared bond of the gate tensor's SVD # l -> left child bond of the TTN node # r -> right child bond of the TTN node # p -> the parent bond of the TTN node @@ -172,19 +173,29 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: # of `canonicalise()`. self.canonicalise(center=(*common_path, DirTTN.LEFT)) - # The overall strategy is to connect the `a` bond of the gate tensor to - # the corresponding bond for `q0` in the TTN (so that its bond `A`) becomes - # the new physical bond for `q0`. However, bonds `b` and `B` corresponding to - # `q1` are left open. We combine this `gate_tensor` with the leaf node of `q0` - # and QR-decompose the result; where the Q tensor will be the new - # (canonicalised) leaf node and R becomes our `msg_tensor`. The latter contains - # the open bonds `b` and `B` and our objective is to "push" this `msg_tensor` + # Apply SVD on the gate tensor to remove any zero singular values ASAP + svd_method = tensor.SVDMethod( + abs_cutoff=self._cfg.zero, + partition="U", # Contract S directly into U + ) + # Apply the SVD decomposition using the configuration defined above + U, S, V = tensor.decompose( + f"{gate_bonds}->SAa,SBb", gate_tensor, method=svd_method, options=options + ) + assert S is None # Due to "partition" option in SVDMethod + + # The overall strategy is to connect the `U` tensor above with the physical bond + # for `q0` in the TTN, so that its bond `A` becomes the new physical bond and + # the bond `S` is left dangling (open). We combine this `gate_tensor` with the + # leaf node of `q0` and QR-decompose the result; where the Q tensor will be the + # new (canonicalised) leaf node and R becomes our `msg_tensor`. The latter + # contains the open bond `S` and our objective is to "push" this `msg_tensor` # through the TTN towards the leaf node of `q1`. Here, "push through" means # contract with the next tensor, and apply QR decomposition, so that the # `msg_tensor` carrying `b` and `B` ends up one bond closer to `q1`. # Once `msg_tensor` is directly connected to the leaf node containing `q1`, we - # just need to contract them, connecting `b` to `q1`, with `B` becoming the - # new physical bond. + # just need to apply the `V` tensor above to `q1` and connect its `S` bond with + # that of the `msg_tensor`. bonds_to_q0 = [ # Bonds in the "arc" from the common ancestor to `q0` path_q0[:i] for i in range(len(common_path) + 1, len(path_q0) + 1) ] @@ -205,14 +216,14 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: assert len(bonds_to_q1) == 1 or len(bonds_to_q1[0]) < len(bonds_to_q1[1]) assert len(bonds_to_q1[-1]) == len(path_q1) - # The `msg_tensor` has four bonds. Our convention will be that the first bond - # always corresponds to `B`, the second bond is `b`, the third bond connects - # it to the TTN in the child direction and the fourth connects it to the TTN - # in the `DirTTN.PARENT` direction. If we label the third bond with `l`, then - # the fourth bond will be labelled `L` (and vice versa). Same for `r` and `p`. + # The `msg_tensor` has three bonds. Our convention will be that the first bond + # always corresponds to `S`, the second bond connects the `msg_tensor` + # to the TTN in the child direction and the third connects it to the TTN + # in the `DirTTN.PARENT` direction. If we label the second bond with `l`, then + # the third bond will be labelled `L` (and vice versa). Same for `r` and `p`. - # We begin applying the gate to the TTN by contracting `gate_tensor` into the - # leaf node containing `q0`, with the `b` and `B` bonds of the latter left open. + # We begin applying the gate to the TTN by contracting `U` into the + # leaf node containing `q0`, with the `S` bond of the former left open. # We immediately QR-decompose the resulting tensor, so that Q becomes the new # (canonicalised) leaf node and R becomes the `msg_tensor` that we will be # "pushing" through the rest of the arc towards `q1`. @@ -223,13 +234,14 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: leaf_bonds = "".join(aux_bonds) + "p" aux_bonds[bond_q0] = "A" Q_bonds = "".join(aux_bonds) + "s" - R_bonds = "Bbsp" # The `msg_tensor` + R_bonds = "Ssp" # The `msg_tensor` + U_bonds = "SAa" # Apply the contraction followed by a QR decomposition leaf_node.tensor, msg_tensor = contract_decompose( - f"{leaf_bonds},{gate_bonds}->{Q_bonds},{R_bonds}", + f"{leaf_bonds},{U_bonds}->{Q_bonds},{R_bonds}", leaf_node.tensor, - gate_tensor, + U, algorithm={"qr_method": tensor.QRMethod()}, options=options, optimize={"path": [(0, 1)]}, @@ -248,9 +260,9 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: node = self.nodes[parent_bond] node_bonds = "lrp" - msg_bonds = "BbLl" if child_dir == DirTTN.LEFT else "BbRr" + msg_bonds = "SLl" if child_dir == DirTTN.LEFT else "SRr" Q_bonds = "Lrs" if child_dir == DirTTN.LEFT else "lRs" - R_bonds = "Bbsp" # The new `msg_tensor` + R_bonds = "Ssp" # The new `msg_tensor` self._logger.debug( f"Pushing msg_tensor ({msg_tensor.nbytes // 2**20} MiB) through node " @@ -277,9 +289,9 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: common_ancestor_node = self.nodes[parent_bond] node_bonds = "lrp" - msg_bonds = "BbLl" if child_dir == DirTTN.LEFT else "BbRr" + msg_bonds = "SLl" if child_dir == DirTTN.LEFT else "SRr" Q_bonds = "Lsp" if child_dir == DirTTN.LEFT else "sRp" - R_bonds = "Bbrs" if child_dir == DirTTN.LEFT else "Bbls" # The new `msg_tensor` + R_bonds = "Srs" if child_dir == DirTTN.LEFT else "Sls" # The new `msg_tensor` self._logger.debug( f"Pushing msg_tensor ({msg_tensor.nbytes // 2**20} MiB) through node " @@ -312,9 +324,9 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: node = self.nodes[parent_bond] node_bonds = "lrp" - msg_bonds = "BbpP" + msg_bonds = "SpP" Q_bonds = "srP" if child_dir == DirTTN.LEFT else "lsP" - R_bonds = "Bbls" if child_dir == DirTTN.LEFT else "Bbrs" # New `msg_tensor` + R_bonds = "Sls" if child_dir == DirTTN.LEFT else "Srs" # New `msg_tensor` self._logger.debug( f"Pushing msg_tensor ({msg_tensor.nbytes // 2**20} MiB) through node " @@ -334,23 +346,25 @@ def _apply_2q_gate(self, q0: Qubit, q1: Qubit, gate: Op) -> TTNxGate: node.canonical_form = child_dir # Finally, the `msg_tensor` is in the parent bond of the leaf node of `q1`. - # All we need to do is contract the `msg_tensor` into the leaf. + # All we need to do is contract the `msg_tensor` and `V` into the leaf. leaf_node = self.nodes[path_q1] n_qbonds = len(leaf_node.tensor.shape) - 1 # Num of qubit bonds aux_bonds = [chr(x) for x in range(n_qbonds)] aux_bonds[bond_q1] = "b" # Connect `b` to `q1` leaf_bonds = "".join(aux_bonds) + "p" - msg_bonds = "BbpP" + msg_bonds = "SpP" + V_bonds = "SBb" aux_bonds[bond_q1] = "B" # `B` becomes the new physical bond `q1` result_bonds = "".join(aux_bonds) + "P" # Apply the contraction leaf_node.tensor = cq.contract( - f"{leaf_bonds},{msg_bonds}->{result_bonds}", + f"{leaf_bonds},{V_bonds},{msg_bonds}->{result_bonds}", leaf_node.tensor, + V, msg_tensor, options=options, - optimize={"path": [(0, 1)]}, + optimize={"path": [(0, 1), (0, 1)]}, ) # The leaf node lost its canonical form leaf_node.canonical_form = None