def _decomposition_1_cnot(U, wires): r"""If there is just one CNOT, we can write the circuit in the form -╭U- = -C--╭C--A- -╰U- = -D--╰X--B- To do this decomposition, first we find G, H in SO(4) such that G (Edag V E) H = (Edag U E) where V depends on the central CNOT gate, and both U, V are in SU(4). This is done following the methods in https://arxiv.org/pdf/quant-ph/0308045.pdf. Once we find G and H, we can use the fact that E SO(4) Edag gives us something in SU(2) x SU(2) to give A, B, C, D. """ # We will actually find a decomposition for the following circuit instead # of the original U # -╭U-╭SWAP- = -C--╭C-╭SWAP--B- # -╰U-╰SWAP- = -D--╰X-╰SWAP--A- # This ensures that the internal part of the decomposition has determinant 1. swap_U = np.exp(1j * np.pi / 4) * math.dot(math.cast_like(SWAP, U), U) # First let's compute gamma(u). For the one-CNOT case, uuT is always real. u = math.dot(math.cast_like(Edag, U), math.dot(swap_U, math.cast_like(E, U))) uuT = math.dot(u, math.T(u)) # Since uuT is real, we can use eigh of its real part. eigh also orders the # eigenvalues in ascending order. _, p = math.linalg.eigh(qml.math.real(uuT)) # Fix the determinant if necessary so that p is in SO(4) p = math.dot(p, math.diag([1, 1, 1, math.sign(math.linalg.det(p))])) # Now, we must find q such that p uu^T p^T = q vv^T q^T. # For this case, our V = SWAP CNOT01 is constant. Thus, we can compute v, # vvT, and its eigenvalues and eigenvectors directly. These matrices are stored # above as the constants v_one_cnot and q_one_cnot. # Once we have p and q properly in SO(4), we compute G and H in SO(4) such # that U = G V H G = math.dot(p, q_one_cnot.T) H = math.dot(math.conj(math.T(v_one_cnot)), math.dot(math.T(G), u)) # We now use the magic basis to convert G, H to SU(2) x SU(2) AB = math.dot(E, math.dot(G, Edag)) CD = math.dot(E, math.dot(H, Edag)) # Extract the tensor prodcts to SU(2) x SU(2) A, B = _su2su2_to_tensor_products(AB) C, D = _su2su2_to_tensor_products(CD) # Recover the operators in the decomposition; note that because of the # initial SWAP, we exchange the order of A and B A_ops = zyz_decomposition(A, wires[1]) B_ops = zyz_decomposition(B, wires[0]) C_ops = zyz_decomposition(C, wires[0]) D_ops = zyz_decomposition(D, wires[1]) return C_ops + D_ops + [qml.CNOT(wires=wires)] + A_ops + B_ops
def _su2su2_to_tensor_products(U): r"""Given a matrix :math:`U = A \otimes B` in SU(2) x SU(2), extract the two SU(2) operations A and B. This process has been described in detail in the Appendix of Coffey & Deiotte https://link.springer.com/article/10.1007/s11128-009-0156-3 """ # First, write A = [[a1, a2], [-a2*, a1*]], which we can do for any SU(2) element. # Then, A \otimes B = [[a1 B, a2 B], [-a2*B, a1*B]] = [[C1, C2], [C3, C4]] # where the Ci are 2x2 matrices. C1 = U[0:2, 0:2] C2 = U[0:2, 2:4] C3 = U[2:4, 0:2] C4 = U[2:4, 2:4] # From the definition of A \otimes B, C1 C4^\dag = a1^2 I, so we can extract a1 C14 = math.dot(C1, math.conj(math.T(C4))) a1 = math.sqrt(math.cast_like(C14[0, 0], 1j)) # Similarly, -C2 C3^\dag = a2^2 I, so we can extract a2 C23 = math.dot(C2, math.conj(math.T(C3))) a2 = math.sqrt(-math.cast_like(C23[0, 0], 1j)) # This gets us a1, a2 up to a sign. To resolve the sign, ensure that # C1 C2^dag = a1 a2* I C12 = math.dot(C1, math.conj(math.T(C2))) if not math.allclose(a1 * math.conj(a2), C12[0, 0]): a2 *= -1 # Construct A A = math.stack( [math.stack([a1, a2]), math.stack([-math.conj(a2), math.conj(a1)])]) # Next, extract B. Can do from any of the C, just need to be careful in # case one of the elements of A is 0. if not math.allclose(A[0, 0], 0.0, atol=1e-6): B = C1 / math.cast_like(A[0, 0], 1j) else: B = C2 / math.cast_like(A[0, 1], 1j) return math.convert_like(A, U), math.convert_like(B, U)
def test_T(t): """Test the simple transpose (T) function""" res = fn.T(t) if isinstance(t, (list, tuple)): t = onp.asarray(t) assert fn.get_interface(res) == fn.get_interface(t) # if tensorflow or pytorch, extract view of underlying data if hasattr(res, "numpy"): res = res.numpy() t = t.numpy() assert np.all(res.T == t.T)
def _compute_num_cnots(U): r"""Compute the number of CNOTs required to implement a U in SU(4). This is based on the trace of .. math:: \gamma(U) = (E^\dag U E) (E^\dag U E)^T, and follows the arguments of this paper: https://arxiv.org/abs/quant-ph/0308045. """ u = math.dot(Edag, math.dot(U, E)) gammaU = math.dot(u, math.T(u)) trace = math.trace(gammaU) # Case: 0 CNOTs (tensor product), the trace is +/- 4 # We need a tolerance of around 1e-7 here in order to work with the case where U # is specified with 8 decimal places. if math.allclose(trace, 4, atol=1e-7) or math.allclose( trace, -4, atol=1e-7): return 0 # To distinguish between 1/2 CNOT cases, we need to look at the eigenvalues evs = math.linalg.eigvals(gammaU) sorted_evs = math.sort(math.imag(evs)) # Case: 1 CNOT, the trace is 0, and the eigenvalues of gammaU are [-1j, -1j, 1j, 1j] # Checking the eigenvalues is needed because of some special 2-CNOT cases that yield # a trace 0. if math.allclose(trace, 0j, atol=1e-7) and math.allclose( sorted_evs, [-1, -1, 1, 1]): return 1 # Case: 2 CNOTs, the trace has only a real part (or is 0) if math.allclose(math.imag(trace), 0.0, atol=1e-7): return 2 # For the case with 3 CNOTs, the trace is a non-zero complex number # with both real and imaginary parts. return 3
def _convert_to_su4(U): r"""Check unitarity of a 4x4 matrix and convert it to :math:`SU(4)` if the determinant is not 1. Args: U (array[complex]): A matrix, presumed to be :math:`4 \times 4` and unitary. Returns: array[complex]: A :math:`4 \times 4` matrix in :math:`SU(4)` that is equivalent to U up to a global phase. """ # Check unitarity if not math.allclose( math.dot(U, math.T(math.conj(U))), math.eye(4), atol=1e-7): raise ValueError("Operator must be unitary.") # Compute the determinant det = math.linalg.det(U) # Convert to SU(4) if it's not close to 1 if not math.allclose(det, 1.0): exp_angle = -1j * math.cast_like(math.angle(det), 1j) / 4 U = math.cast_like(U, det) * math.exp(exp_angle) return U
def _convert_to_su2(U): r"""Check unitarity of a matrix and convert it to :math:`SU(2)` if possible. Args: U (array[complex]): A matrix, presumed to be :math:`2 \times 2` and unitary. Returns: array[complex]: A :math:`2 \times 2` matrix in :math:`SU(2)` that is equivalent to U up to a global phase. """ # Check unitarity if not math.allclose( math.dot(U, math.T(math.conj(U))), math.eye(2), atol=1e-7): raise ValueError("Operator must be unitary.") # Compute the determinant det = U[0, 0] * U[1, 1] - U[0, 1] * U[1, 0] # Convert to SU(2) if it's not close to 1 if not math.allclose(det, [1.0]): exp_angle = -1j * math.cast_like(math.angle(det), 1j) / 2 U = math.cast_like(U, exp_angle) * math.exp(exp_angle) return U
def adjoint(self, do_queue=False): U = self.parameters[0] return Interferometer(qml_math.T(qml_math.conj(U)), wires=self.wires, do_queue=do_queue)
def _decomposition_3_cnots(U, wires): r"""The most general form of this decomposition is U = (A \otimes B) V (C \otimes D), where V is as depicted in the circuit below: -╭U- = -C--╭X--RZ(d)--╭C---------╭X--A- -╰U- = -D--╰C--RY(b)--╰X--RY(a)--╰C--B- """ # First we add a SWAP as per v1 of arXiv:0308033, which helps with some # rearranging of gates in the decomposition (it will cancel out the fact # that we need to add a SWAP to fix the determinant in another part later). swap_U = np.exp(1j * np.pi / 4) * math.dot(math.cast_like(SWAP, U), U) # Choose the rotation angles of RZ, RY in the two-qubit decomposition. # They are chosen as per Proposition V.1 in quant-ph/0308033 and are based # on the phases of the eigenvalues of :math:`E^\dagger \gamma(U) E`, where # \gamma(U) = (E^\dag U E) (E^\dag U E)^T. # The rotation angles can be computed as follows (any three eigenvalues can be used) u = math.dot(Edag, math.dot(swap_U, E)) gammaU = math.dot(u, math.T(u)) evs, _ = math.linalg.eig(gammaU) # We will sort the angles so that results are consistent across interfaces. angles = math.sort([math.angle(ev) for ev in evs]) x, y, z = angles[0], angles[1], angles[2] # Compute functions of the eigenvalues; there are different options in v1 # vs. v3 of the paper, I'm not entirely sure why. This is the version from v3. alpha = (x + y) / 2 beta = (x + z) / 2 delta = (z + y) / 2 # This is the interior portion of the decomposition circuit interior_decomp = [ qml.CNOT(wires=[wires[1], wires[0]]), qml.RZ(delta, wires=wires[0]), qml.RY(beta, wires=wires[1]), qml.CNOT(wires=wires), qml.RY(alpha, wires=wires[1]), qml.CNOT(wires=[wires[1], wires[0]]), ] # We need the matrix representation of this interior part, V, in order to # decompose U = (A \otimes B) V (C \otimes D) # # Looking at the decomposition above, V has determinant -1 (because there # are 3 CNOTs, each with determinant -1). The relationship between U and V # requires that both are in SU(4), so we add a SWAP after to V. We will see # how this gets fixed later. # # -╭V- = -╭X--RZ(d)--╭C---------╭X--╭SWAP- # -╰V- = -╰C--RY(b)--╰X--RY(a)--╰C--╰SWAP- RZd = qml.RZ(math.cast_like(delta, 1j), wires=wires[0]).matrix RYb = qml.RY(beta, wires=wires[0]).matrix RYa = qml.RY(alpha, wires=wires[0]).matrix V_mats = [ CNOT10, math.kron(RZd, RYb), CNOT01, math.kron(math.eye(2), RYa), CNOT10, SWAP ] V = math.convert_like(math.eye(4), U) for mat in V_mats: V = math.dot(math.cast_like(mat, U), V) # Now we need to find the four SU(2) operations A, B, C, D A, B, C, D = _extract_su2su2_prefactors(swap_U, V) # At this point, we have the following: # -╭U-╭SWAP- = --C--╭X-RZ(d)-╭C-------╭X-╭SWAP--A # -╰U-╰SWAP- = --D--╰C-RZ(b)-╰X-RY(a)-╰C-╰SWAP--B # # Using the relationship that SWAP(A \otimes B) SWAP = B \otimes A, # -╭U-╭SWAP- = --C--╭X-RZ(d)-╭C-------╭X--B--╭SWAP- # -╰U-╰SWAP- = --D--╰C-RZ(b)-╰X-RY(a)-╰C--A--╰SWAP- # # Now the SWAPs cancel, giving us the desired decomposition # (up to a global phase). # -╭U- = --C--╭X-RZ(d)-╭C-------╭X--B-- # -╰U- = --D--╰C-RZ(b)-╰X-RY(a)-╰C--A-- A_ops = zyz_decomposition(A, wires[1]) B_ops = zyz_decomposition(B, wires[0]) C_ops = zyz_decomposition(C, wires[0]) D_ops = zyz_decomposition(D, wires[1]) # Return the full decomposition return C_ops + D_ops + interior_decomp + A_ops + B_ops
def _decomposition_2_cnots(U, wires): r"""If 2 CNOTs are required, we can write the circuit as -╭U- = -A--╭X--RZ(d)--╭X--C- -╰U- = -B--╰C--RX(p)--╰C--D- We need to find the angles for the Z and X rotations such that the inner part has the same spectrum as U, and then we can recover A, B, C, D. """ # Compute the rotation angles u = math.dot(Edag, math.dot(U, E)) gammaU = math.dot(u, math.T(u)) evs, _ = math.linalg.eig(gammaU) # These choices are based on Proposition III.3 of # https://arxiv.org/abs/quant-ph/0308045 # There is, however, a special case where the circuit has the form # -╭U- = -A--╭C--╭X--C- # -╰U- = -B--╰X--╰C--D- # # or some variant of this, where the two CNOTs are adjacent. # # What happens here is that the set of evs is -1, -1, 1, 1 and we can write # -╭U- = -A--╭X--SZ--╭X--C- # -╰U- = -B--╰C--SX--╰C--D- # where SZ and SX are square roots of Z and X respectively. (This # decomposition comes from using Hadamards to flip the direction of the # first CNOT, and then decomposing them and merging single-qubit gates.) For # some reason this case is not handled properly with the full algorithm, so # we treat it separately. sorted_evs = math.sort(math.real(evs)) if math.allclose(sorted_evs, [-1, -1, 1, 1]): interior_decomp = [ qml.CNOT(wires=[wires[1], wires[0]]), qml.S(wires=wires[0]), qml.SX(wires=wires[1]), qml.CNOT(wires=[wires[1], wires[0]]), ] # S \otimes SX inner_matrix = S_SX else: # For the non-special case, the eigenvalues come in conjugate pairs. # We need to find two non-conjugate eigenvalues to extract the angles. x = math.angle(evs[0]) y = math.angle(evs[1]) # If it was the conjugate, grab a different eigenvalue. if math.allclose(x, -y): y = math.angle(evs[2]) delta = (x + y) / 2 phi = (x - y) / 2 interior_decomp = [ qml.CNOT(wires=[wires[1], wires[0]]), qml.RZ(delta, wires=wires[0]), qml.RX(phi, wires=wires[1]), qml.CNOT(wires=[wires[1], wires[0]]), ] RZd = qml.RZ(math.cast_like(delta, 1j), wires=0).matrix RXp = qml.RX(phi, wires=0).matrix inner_matrix = math.kron(RZd, RXp) # We need the matrix representation of this interior part, V, in order to # decompose U = (A \otimes B) V (C \otimes D) V = math.dot(math.cast_like(CNOT10, U), math.dot(inner_matrix, math.cast_like(CNOT10, U))) # Now we find the A, B, C, D in SU(2), and return the decomposition A, B, C, D = _extract_su2su2_prefactors(U, V) A_ops = zyz_decomposition(A, wires[0]) B_ops = zyz_decomposition(B, wires[1]) C_ops = zyz_decomposition(C, wires[0]) D_ops = zyz_decomposition(D, wires[1]) return C_ops + D_ops + interior_decomp + A_ops + B_ops
def _extract_su2su2_prefactors(U, V): r"""This function is used for the case of 2 CNOTs and 3 CNOTs. It does something similar as the 1-CNOT case, but there is no special form for one of the SO(4) operations. Suppose U, V are SU(4) matrices for which there exists A, B, C, D such that (A \otimes B) V (C \otimes D) = U. The problem is to find A, B, C, D in SU(2) in an analytic and fully differentiable manner. This decomposition is possible when U and V are in the same double coset of SU(4), meaning there exists G, H in SO(4) s.t. G (Edag V E) H = (Edag U E). This is guaranteed here by how V was constructed in both the _decomposition_2_cnots and _decomposition_3_cnots methods. Then, we can use the fact that E SO(4) Edag gives us something in SU(2) x SU(2) to give A, B, C, D. """ # A lot of the work here happens in the magic basis. Essentially, we # don't look explicitly at some U = G V H, but rather at # E^\dagger U E = G E^\dagger V E H # so that we can recover # U = (E G E^\dagger) V (E H E^\dagger) = (A \otimes B) V (C \otimes D). # There is some math in the paper explaining how when we define U in this way, # we can simultaneously diagonalize functions of U and V to ensure they are # in the same coset and recover the decomposition. u = math.dot(math.cast_like(Edag, V), math.dot(U, math.cast_like(E, V))) v = math.dot(math.cast_like(Edag, V), math.dot(V, math.cast_like(E, V))) uuT = math.dot(u, math.T(u)) vvT = math.dot(v, math.T(v)) # Get the p and q in SO(4) that diagonalize uuT and vvT respectively (and # their eigenvalues). We are looking for a simultaneous diagonalization, # which we know exists because of how U and V were constructed. Furthermore, # The way we will do this is by noting that, since uuT/vvT are complex and # symmetric, so both their real and imaginary parts share a set of # real-valued eigenvectors, which are also eigenvectors of uuT/vvT # themselves. So we can use eigh, which orders the eigenvectors, and so we # are guaranteed that the p and q returned will be "in the same order". _, p = math.linalg.eigh(math.real(uuT) + math.imag(uuT)) _, q = math.linalg.eigh(math.real(vvT) + math.imag(vvT)) # If determinant of p/q is not 1, it is in O(4) but not SO(4), and has determinant # We can transform it to SO(4) by simply negating one of the columns. p = math.dot(p, math.diag([1, 1, 1, math.sign(math.linalg.det(p))])) q = math.dot(q, math.diag([1, 1, 1, math.sign(math.linalg.det(q))])) # Now, we should have p, q in SO(4) such that p^T u u^T p = q^T v v^T q. # Then (v^\dag q p^T u)(v^\dag q p^T u)^T = I. # So we can set G = p q^T, H = v^\dag q p^T u to obtain G v H = u. G = math.dot(math.cast_like(p, 1j), math.T(q)) H = math.dot(math.conj(math.T(v)), math.dot(math.T(G), u)) # These are still in SO(4) though - we want to convert things into SU(2) x SU(2) # so use the entangler. Since u = E^\dagger U E and v = E^\dagger V E where U, V # are the target matrices, we can reshuffle as in the docstring above, # U = (E G E^\dagger) V (E H E^\dagger) = (A \otimes B) V (C \otimes D) # where A, B, C, D are in SU(2) x SU(2). AB = math.dot(math.cast_like(E, G), math.dot(G, math.cast_like(Edag, G))) CD = math.dot(math.cast_like(E, H), math.dot(H, math.cast_like(Edag, H))) # Now, we just need to extract the constituent tensor products. A, B = _su2su2_to_tensor_products(AB) C, D = _su2su2_to_tensor_products(CD) return A, B, C, D