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
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 zyz_decomposition(U, wire): r"""Recover the decomposition of a single-qubit matrix :math:`U` in terms of elementary operations. Diagonal operations will be converted to a single :class:`.RZ` gate, while non-diagonal operations will be converted to a :class:`.Rot` gate that implements the original operation up to a global phase in the form :math:`RZ(\omega) RY(\theta) RZ(\phi)`. Args: U (tensor): A 2 x 2 unitary matrix. wire (Union[Wires, Sequence[int] or int]): The wire on which to apply the operation. Returns: list[qml.Operation]: A ``Rot`` gate on the specified wire that implements ``U`` up to a global phase, or an equivalent ``RZ`` gate if ``U`` is diagonal. **Example** Suppose we would like to apply the following unitary operation: .. code-block:: python3 U = np.array([ [-0.28829348-0.78829734j, 0.30364367+0.45085995j], [ 0.53396245-0.10177564j, 0.76279558-0.35024096j] ]) For PennyLane devices that cannot natively implement ``QubitUnitary``, we can instead recover a ``Rot`` gate that implements the same operation, up to a global phase: >>> decomp = zyz_decomposition(U, 0) >>> decomp [Rot(-0.24209529417800013, 1.14938178234275, 1.7330581433950871, wires=[0])] """ U = _convert_to_su2(U) # Check if the matrix is diagonal; only need to check one corner. # If it is diagonal, we don't need a full Rot, just return an RZ. if math.allclose(U[0, 1], [0.0]): omega = 2 * math.angle(U[1, 1]) return [qml.RZ(omega, wires=wire)] # If the top left element is 0, can only use the off-diagonal elements. We # have to be very careful with the math here to ensure things that get # multiplied together are of the correct type in the different interfaces. if math.allclose(U[0, 0], [0.0]): phi = 0.0 theta = -np.pi omega = 1j * math.log(U[0, 1] / U[1, 0]) - np.pi else: # If not diagonal, compute the angle of the RY cos2_theta_over_2 = math.abs(U[0, 0] * U[1, 1]) theta = 2 * math.arccos(math.sqrt(cos2_theta_over_2)) el_division = U[0, 0] / U[1, 0] tan_part = math.cast_like(math.tan(theta / 2), el_division) omega = 1j * math.log(tan_part * el_division) phi = -omega - math.cast_like(2 * math.angle(U[0, 0]), omega) return [ qml.Rot(math.real(phi), math.real(theta), math.real(omega), wires=wire) ]