From acb3357d7044233a9a748d3fc584485e3b7b8e40 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 22 Jun 2021 14:08:30 -0400 Subject: [PATCH 01/37] Add decomposition for QubitUnitary. --- pennylane/ops/qubit.py | 56 ++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 32710eece20..d27fbebed9b 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -476,14 +476,7 @@ class CY(Operation): num_params = 0 num_wires = 2 par_domain = None - matrix = np.array( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, -1j], - [0, 0, 1j, 0], - ] - ) + matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0],]) @classmethod def _matrix(cls, *params): @@ -1202,8 +1195,7 @@ def _matrix(cls, *params): # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( - np.kron, - [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], + np.kron, [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], ) return expand( @@ -1498,14 +1490,7 @@ def _matrix(cls, *params): @classmethod def _eigvals(cls, *params): theta = params[0] - return np.array( - [ - 1, - 1, - cmath.exp(-0.5j * theta), - cmath.exp(0.5j * theta), - ] - ) + return np.array([1, 1, cmath.exp(-0.5j * theta), cmath.exp(0.5j * theta),]) @staticmethod def decomposition(lam, wires): @@ -2157,6 +2142,41 @@ def _matrix(cls, *params): return U + @staticmethod + def decomposition(U, wires): + # Single-qubit unitaries + if U.shape[0] == 2: + # Check validity of input + U = QubitUnitary._matrix(U) + + wires = Wires(wires) + + # First remove the global phase; cannot just divide by the square root + # because sometimes the determinant of a unitary matrix is negative. + det = np.linalg.det(U) + U = U * (np.exp(-1j * np.angle(det) / 2)) + + # Compute the angle of the Y rotation + theta = 2 * np.arcsin(np.abs(U[0, 1])) + + # If it's close to 0, the matrix is diagonal and we have just an RZ rotation + if np.isclose(theta, 0): + omega = 2 * np.angle(U[0, 0]) + return [RZ(omega, wires=wires[0])] + + # If not diagonal, we actually have to work out the details and recover + # a decomposition of the form RZ(omega) RY(theta) RZ(phi) + if np.isclose(U[0, 0], 0): + phi = (1j * np.log(U[0, 1] / U[1, 0])).real + omega = -phi - 2 * np.angle(U[1, 0]) + else: + omega = (1j * np.log(np.tan(theta / 2) * U[0, 0] / U[1, 0])).real + phi = -omega - 2 * np.angle(U[0, 0]) + + return [RZ(phi, wires=wires[0]), RY(theta, wires=wires[0]), RZ(omega, wires=wires[0])] + else: + return NotImplementedError + def adjoint(self): return QubitUnitary(qml.math.T(qml.math.conj(self.matrix)), wires=self.wires) From 62863e1d228066286c4dedde0fda03f86cb6fcdb Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 22 Jun 2021 15:17:54 -0400 Subject: [PATCH 02/37] Add tests. Rewrite decomposition as Rot instead of RZ,RY,RZ. --- pennylane/ops/qubit.py | 42 +++++++++++++++++++++++++------------ tests/ops/test_qubit_ops.py | 23 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index d27fbebed9b..92c593d509c 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -476,7 +476,14 @@ class CY(Operation): num_params = 0 num_wires = 2 par_domain = None - matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0],]) + matrix = np.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, -1j], + [0, 0, 1j, 0], + ] + ) @classmethod def _matrix(cls, *params): @@ -1195,7 +1202,8 @@ def _matrix(cls, *params): # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( - np.kron, [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], + np.kron, + [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], ) return expand( @@ -1490,7 +1498,14 @@ def _matrix(cls, *params): @classmethod def _eigvals(cls, *params): theta = params[0] - return np.array([1, 1, cmath.exp(-0.5j * theta), cmath.exp(0.5j * theta),]) + return np.array( + [ + 1, + 1, + cmath.exp(-0.5j * theta), + cmath.exp(0.5j * theta), + ] + ) @staticmethod def decomposition(lam, wires): @@ -2144,28 +2159,29 @@ def _matrix(cls, *params): @staticmethod def decomposition(U, wires): - # Single-qubit unitaries + # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ if U.shape[0] == 2: - # Check validity of input + # Checks validity of input U = QubitUnitary._matrix(U) wires = Wires(wires) - # First remove the global phase; cannot just divide by the square root - # because sometimes the determinant of a unitary matrix is negative. + # Remove the global phase if present; cannot just divide by square root of det + # because sometimes it is negative. det = np.linalg.det(U) - U = U * (np.exp(-1j * np.angle(det) / 2)) + if not np.isclose(det, 1): + U = U * (np.exp(-1j * np.angle(det) / 2)) - # Compute the angle of the Y rotation + # Compute the angle of the RY theta = 2 * np.arcsin(np.abs(U[0, 1])) # If it's close to 0, the matrix is diagonal and we have just an RZ rotation if np.isclose(theta, 0): - omega = 2 * np.angle(U[0, 0]) + omega = 2 * np.angle(U[1, 1]) return [RZ(omega, wires=wires[0])] - # If not diagonal, we actually have to work out the details and recover - # a decomposition of the form RZ(omega) RY(theta) RZ(phi) + # Otherwise recover the decomposition as a Rot, which can be further decomposed + # if desired. If the top left element is 0, can only use the off-diagonal elements if np.isclose(U[0, 0], 0): phi = (1j * np.log(U[0, 1] / U[1, 0])).real omega = -phi - 2 * np.angle(U[1, 0]) @@ -2173,7 +2189,7 @@ def decomposition(U, wires): omega = (1j * np.log(np.tan(theta / 2) * U[0, 0] / U[1, 0])).real phi = -omega - 2 * np.angle(U[0, 0]) - return [RZ(phi, wires=wires[0]), RY(theta, wires=wires[0]), RZ(omega, wires=wires[0])] + return [qml.Rot(phi, theta, omega, wires=wires[0])] else: return NotImplementedError diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py index 8446595a0c0..5f6b2b89fe6 100644 --- a/tests/ops/test_qubit_ops.py +++ b/tests/ops/test_qubit_ops.py @@ -1445,6 +1445,29 @@ def test_qubit_unitary_not_matrix_exception(self, U): with pytest.raises(ValueError, match="must be a square matrix"): qml.QubitUnitary(U, wires=0).matrix + @pytest.mark.parametrize( + "U,expected_gate,expected_params", + [ # First set of gates are diagonal and converted to RZ + (I, qml.RZ, [0]), + (Z, qml.RZ, [np.pi]), + (S, qml.RZ, [np.pi / 2]), + (T, qml.RZ, [np.pi / 4]), + (qml.RZ(0.3, wires=0).matrix, qml.RZ, [0.3]), + (qml.RZ(-0.5, wires=0).matrix, qml.RZ, [-0.5]), + # Next set of gates are non-diagonal and decomposed as Rots + (H, qml.Rot, [np.pi, np.pi / 2, 0]), + (X, qml.Rot, [0.0, np.pi, np.pi]), + (qml.Rot(0.2, 0.5, -0.3, wires=0).matrix, qml.Rot, [0.2, 0.5, -0.3]), + (np.exp(1j * 0.02) * qml.Rot(-1, 2, -3, wires=0).matrix, qml.Rot, [-1, 2, -3]), + ], + ) + def test_qubit_unitary_decomposition(self, U, expected_gate, expected_params): + decomp = qml.QubitUnitary.decomposition(U, wires=0) + + assert len(decomp) == 1 + assert isinstance(decomp[0], expected_gate) + assert np.allclose(decomp[0].parameters, expected_params) + def test_iswap_eigenval(self): """Tests that the ISWAP eigenvalue matches the numpy eigenvalues of the ISWAP matrix""" op = qml.ISWAP(wires=[0, 1]) From 5dbe2c0a4ecbeee8d6c53dc14c1d2e62d26b897b Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 22 Jun 2021 16:03:19 -0400 Subject: [PATCH 03/37] Update decomposition to use qml.math for differentiability. --- pennylane/ops/qubit.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 92c593d509c..14c59f862f6 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2161,35 +2161,36 @@ def _matrix(cls, *params): def decomposition(U, wires): # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ if U.shape[0] == 2: - # Checks validity of input - U = QubitUnitary._matrix(U) - wires = Wires(wires) # Remove the global phase if present; cannot just divide by square root of det # because sometimes it is negative. - det = np.linalg.det(U) - if not np.isclose(det, 1): - U = U * (np.exp(-1j * np.angle(det) / 2)) + det = U[0, 0] * U[1, 1] - U[0, 1] * U[1, 0] + if not qml.math.isclose(det, 1): + U = U * (qml.math.exp(-1j * qml.math.angle(det) / 2)) # Compute the angle of the RY - theta = 2 * np.arcsin(np.abs(U[0, 1])) + theta = 2 * qml.math.arccos(qml.math.sqrt(U[0, 0] * U[1, 1])) # If it's close to 0, the matrix is diagonal and we have just an RZ rotation - if np.isclose(theta, 0): - omega = 2 * np.angle(U[1, 1]) + if qml.math.isclose(theta, 0, atol=1e-7): + omega = 2 * qml.math.angle(U[1, 1]) return [RZ(omega, wires=wires[0])] # Otherwise recover the decomposition as a Rot, which can be further decomposed # if desired. If the top left element is 0, can only use the off-diagonal elements - if np.isclose(U[0, 0], 0): - phi = (1j * np.log(U[0, 1] / U[1, 0])).real - omega = -phi - 2 * np.angle(U[1, 0]) + if qml.math.isclose(U[0, 0], 0): + phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) + omega = -phi - 2 * qml.math.angle(U[1, 0]) else: - omega = (1j * np.log(np.tan(theta / 2) * U[0, 0] / U[1, 0])).real - phi = -omega - 2 * np.angle(U[0, 0]) + omega = 1j * qml.math.log(qml.math.tan(theta / 2) * U[0, 0] / U[1, 0]) + phi = -omega - 2 * qml.math.angle(U[0, 0]) - return [qml.Rot(phi, theta, omega, wires=wires[0])] + return [ + qml.Rot( + qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wires[0] + ) + ] else: return NotImplementedError From 2772e522dab9fbf7423faf6c688abee2580c3963 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:15:39 -0400 Subject: [PATCH 04/37] Apply suggestions from code review Co-authored-by: Josh Izaac --- pennylane/ops/qubit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 14c59f862f6..5336c30dd1a 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2160,7 +2160,7 @@ def _matrix(cls, *params): @staticmethod def decomposition(U, wires): # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ - if U.shape[0] == 2: + if qml.math.shape(U)[0] == 2: wires = Wires(wires) # Remove the global phase if present; cannot just divide by square root of det @@ -2191,8 +2191,7 @@ def decomposition(U, wires): qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wires[0] ) ] - else: - return NotImplementedError + return NotImplementedError("Decompositions only supported for single-qubit unitaries") def adjoint(self): return QubitUnitary(qml.math.T(qml.math.conj(self.matrix)), wires=self.wires) From c90439a2c48b54bac52e2b27c0f884bdb6122693 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 09:19:54 -0400 Subject: [PATCH 05/37] Re-implement decomposition as a transform. --- pennylane/ops/qubit.py | 32 +------- pennylane/transforms/__init__.py | 3 +- pennylane/transforms/decompositions.py | 103 +++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 pennylane/transforms/decompositions.py diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 5336c30dd1a..f9dca4f4b3d 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2161,36 +2161,10 @@ def _matrix(cls, *params): def decomposition(U, wires): # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ if qml.math.shape(U)[0] == 2: - wires = Wires(wires) - - # Remove the global phase if present; cannot just divide by square root of det - # because sometimes it is negative. - det = U[0, 0] * U[1, 1] - U[0, 1] * U[1, 0] - if not qml.math.isclose(det, 1): - U = U * (qml.math.exp(-1j * qml.math.angle(det) / 2)) - - # Compute the angle of the RY - theta = 2 * qml.math.arccos(qml.math.sqrt(U[0, 0] * U[1, 1])) - - # If it's close to 0, the matrix is diagonal and we have just an RZ rotation - if qml.math.isclose(theta, 0, atol=1e-7): - omega = 2 * qml.math.angle(U[1, 1]) - return [RZ(omega, wires=wires[0])] - - # Otherwise recover the decomposition as a Rot, which can be further decomposed - # if desired. If the top left element is 0, can only use the off-diagonal elements - if qml.math.isclose(U[0, 0], 0): - phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) - omega = -phi - 2 * qml.math.angle(U[1, 0]) - else: - omega = 1j * qml.math.log(qml.math.tan(theta / 2) * U[0, 0] / U[1, 0]) - phi = -omega - 2 * qml.math.angle(U[0, 0]) + wire = Wires(wires)[0] + decomp_ops = qml.transforms.decompositions._zyz_decomposition(U, wire) + return decomp_ops - return [ - qml.Rot( - qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wires[0] - ) - ] return NotImplementedError("Decompositions only supported for single-qubit unitaries") def adjoint(self): diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index 0460240dd09..e797bdfe417 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -75,14 +75,15 @@ ~qfunc_transform ~transforms.make_tape """ +from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform from .adjoint import adjoint from .classical_jacobian import classical_jacobian from .control import ControlledOperation, ctrl +from .decompositions import decompose_single_qubit_unitaries from .draw import draw from .hamiltonian_expand import hamiltonian_expand from .invisible import invisible from .measurement_grouping import measurement_grouping from .metric_tensor import metric_tensor, metric_tensor_tape from .specs import specs -from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform from .qmc import apply_controlled_Q, quantum_monte_carlo diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py new file mode 100644 index 00000000000..6e1e78bdae4 --- /dev/null +++ b/pennylane/transforms/decompositions.py @@ -0,0 +1,103 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains transforms for decomposing arbitrary unitary operations into elementary gates. +""" +import pennylane as qml +from pennylane.transforms import qfunc_transform + + +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 dimensions + if qml.math.shape(U)[0] != 2 or qml.math.shape(U)[1] != 2: + raise ValueError("Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).") + + # Check unitarity + if not qml.math.allclose(qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2)): + 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 qml.math.isclose(det, 1): + U = U * (qml.math.exp(-1j * qml.math.angle(det) / 2)) + + return U + + +def _zyz_decomposition(U, wire): + r"""Helper function to recover the rotation angles of a single-qubit matrix :math:`U`. + + The set of angles are chosen so as to implement :math:`U` up to a global phase. + + Args: + U (tensor): A 2 x 2 unitary matrix. + + Returns: + (float, float, float): A set of angles (:math:`\phi`, :math:`\theta`, :math:`\omega`) + that implement U as a sequence :math:`U = RZ(\omega) RY(\theta) RZ(\phi)`. + """ + U = _convert_to_su2(U) + + # Compute the angle of the RY + theta = 2 * qml.math.arccos(qml.math.sqrt(U[0, 0] * U[1, 1])) + + # If it's close to 0, the matrix is diagonal and we have just an RZ rotation + if qml.math.isclose(theta, 0, atol=1e-7): + omega = 2 * qml.math.angle(U[1, 1]) + return [qml.RZ(omega, wires=wire)] + + # Otherwise recover the decomposition as a Rot, which can be further decomposed + # if desired. If the top left element is 0, can only use the off-diagonal elements + if qml.math.isclose(U[0, 0], 0): + phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) + omega = -phi - 2 * qml.math.angle(U[1, 0]) + else: + omega = 1j * qml.math.log(qml.math.tan(theta / 2) * U[0, 0] / U[1, 0]) + phi = -omega - 2 * qml.math.angle(U[0, 0]) + + return [qml.Rot(qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wire)] + + +@qfunc_transform +def decompose_single_qubit_unitaries(tape): + """Quantum function transform to decomposes all instances of single-qubit QubitUnitary + operations to a sequence of rotations of the form ``RZ``, ``RY``, ``RZ``. + + Args: + tape (qml.tape.QuantumTape): A quantum tape. + """ + for op in tape.operations + tape.measurements: + if isinstance(op, qml.QubitUnitary): + dim_U = qml.math.shape(op.parameters[0])[0] + + if dim_U != 2: + continue + + decomp = _zyz_decomposition(op.parameters[0], op.wire) + + for d_op in decomp: + d_op.queue() + else: + op.queue() From f3c5bd0291b28443c99c48cef984361de5b89b95 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 14:57:52 -0400 Subject: [PATCH 06/37] Rework code to work with different interfaces. --- pennylane/transforms/decompositions.py | 35 ++++--- tests/transforms/test_decompositions.py | 116 ++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 tests/transforms/test_decompositions.py diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 6e1e78bdae4..53f96b383a6 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -15,10 +15,11 @@ Contains transforms for decomposing arbitrary unitary operations into elementary gates. """ import pennylane as qml +import numpy as np from pennylane.transforms import qfunc_transform -def _convert_to_su2(U): +def _convert_to_su2(U, zero_tol=1e-6): r"""Check unitarity of a matrix and convert it to :math:`SU(2)` if possible. Args: @@ -33,49 +34,59 @@ def _convert_to_su2(U): raise ValueError("Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).") # Check unitarity - if not qml.math.allclose(qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2)): + if not qml.math.allclose( + qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=zero_tol + ): 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 qml.math.isclose(det, 1): - U = U * (qml.math.exp(-1j * qml.math.angle(det) / 2)) + if not qml.math.allclose(det, [1.0], atol=zero_tol): + exp_angle = -1j * qml.math.cast_like(qml.math.angle(det), 1j) / 2 + U = U * (qml.math.exp(exp_angle)) return U -def _zyz_decomposition(U, wire): +def _zyz_decomposition(U, wire, zero_tol=1e-6): r"""Helper function to recover the rotation angles of a single-qubit matrix :math:`U`. The set of angles are chosen so as to implement :math:`U` up to a global phase. Args: U (tensor): A 2 x 2 unitary matrix. + tol (float): The tolerance at which an angle is considered to be close enough + to 0. Needed to deal with varying precision across interfaces. Returns: (float, float, float): A set of angles (:math:`\phi`, :math:`\theta`, :math:`\omega`) that implement U as a sequence :math:`U = RZ(\omega) RY(\theta) RZ(\phi)`. """ - U = _convert_to_su2(U) + U = _convert_to_su2(U, zero_tol) # Compute the angle of the RY - theta = 2 * qml.math.arccos(qml.math.sqrt(U[0, 0] * U[1, 1])) + cos2_theta_over_2 = qml.math.abs(U[0, 0] * U[1, 1]) + theta = 2 * qml.math.arccos(qml.math.sqrt(cos2_theta_over_2)) # If it's close to 0, the matrix is diagonal and we have just an RZ rotation - if qml.math.isclose(theta, 0, atol=1e-7): + if qml.math.allclose(theta, [0.0], atol=zero_tol): omega = 2 * qml.math.angle(U[1, 1]) return [qml.RZ(omega, wires=wire)] # Otherwise recover the decomposition as a Rot, which can be further decomposed # if desired. If the top left element is 0, can only use the off-diagonal elements - if qml.math.isclose(U[0, 0], 0): + # We have to be very careful with the math here to ensure things that get multiplied + # together are of the correct type. + if qml.math.allclose(U[0, 0], [0.0], atol=zero_tol): phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) - omega = -phi - 2 * qml.math.angle(U[1, 0]) + omega = -phi - qml.math.cast_like(2 * qml.math.angle(U[1, 0]), phi) else: - omega = 1j * qml.math.log(qml.math.tan(theta / 2) * U[0, 0] / U[1, 0]) - phi = -omega - 2 * qml.math.angle(U[0, 0]) + el_division = U[0, 0] / U[1, 0] + tan_part = qml.math.cast_like(qml.math.tan(theta / 2), el_division) + omega = 1j * qml.math.log(tan_part * el_division) + phi = -omega - qml.math.cast_like(2 * qml.math.angle(U[0, 0]), omega) return [qml.Rot(qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wire)] diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py new file mode 100644 index 00000000000..ad010403181 --- /dev/null +++ b/tests/transforms/test_decompositions.py @@ -0,0 +1,116 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for the QubitUnitary decomposition transforms. +""" + +import pytest + +import pennylane as qml +from pennylane import numpy as np + +from pennylane.wires import Wires +from pennylane.transforms import decompose_single_qubit_unitaries +from pennylane.transforms.decompositions import _zyz_decomposition + +from gate_data import I, Z, S, T, H, X + +single_qubit_decomps = [ + # First set of gates are diagonal and converted to RZ + (I, qml.RZ, [0.0]), + (Z, qml.RZ, [np.pi]), + (S, qml.RZ, [np.pi / 2]), + (T, qml.RZ, [np.pi / 4]), + (qml.RZ(0.3, wires=0).matrix, qml.RZ, [0.3]), + (qml.RZ(-0.5, wires=0).matrix, qml.RZ, [-0.5]), + # Next set of gates are non-diagonal and decomposed as Rots + (H, qml.Rot, [np.pi, np.pi / 2, 0.0]), + (X, qml.Rot, [0.0, np.pi, np.pi]), + (qml.Rot(0.2, 0.5, -0.3, wires=0).matrix, qml.Rot, [0.2, 0.5, -0.3]), + (np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix, qml.Rot, [-1.0, 2.0, -3.0]), +] + + +class TestQubitUnitaryDecompositionHelpers: + """Test that the decompsoitions are correct.""" + + def test_zyz_decomposition_invalid_input(self): + with pytest.raises(ValueError, match="Operator must be unitary"): + _zyz_decomposition(I + H, Wires("a")) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_zyz_decomposition(self, U, expected_gate, expected_params): + """Test that a one-qubit matrix in isolation is correctly decomposed.""" + obtained_gates = _zyz_decomposition(U, Wires("a")) + + assert len(obtained_gates) == 1 + + assert isinstance(obtained_gates[0], expected_gate) + assert obtained_gates[0].wires == Wires("a") + assert qml.math.allclose(obtained_gates[0].parameters, expected_params) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_zyz_decomposition_torch(self, U, expected_gate, expected_params): + """Test that a one-qubit operation in Torch is correctly decomposed.""" + torch = pytest.importorskip("torch") + + U = torch.tensor(U, dtype=torch.complex128) + + obtained_gates = _zyz_decomposition(U, wire="a") + + assert len(obtained_gates) == 1 + + obtained_params = [x.detach() for x in obtained_gates[0].parameters] + + assert isinstance(obtained_gates[0], expected_gate) + assert obtained_gates[0].wires == Wires("a") + assert qml.math.allclose(obtained_params, expected_params) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_zyz_decomposition_tf(self, U, expected_gate, expected_params): + """Test that a one-qubit operation in Tensorflow is correctly decomposed.""" + tf = pytest.importorskip("tensorflow") + + U = tf.Variable(U, dtype=tf.complex128) + + obtained_gates = _zyz_decomposition(U, wire="a") + + assert len(obtained_gates) == 1 + + obtained_params = [x.numpy() for x in obtained_gates[0].parameters] + + assert isinstance(obtained_gates[0], expected_gate) + assert obtained_gates[0].wires == Wires("a") + assert qml.math.allclose(obtained_params, expected_params) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): + """Test that a one-qubit operation in JAX is correctly decomposed.""" + jax = pytest.importorskip("jax") + from jax import numpy as jnp + + U = jnp.array(U, dtype=jnp.complex64) + + obtained_gates = _zyz_decomposition(U, wire="a") + + assert len(obtained_gates) == 1 + + obtained_params = [jnp.asarray(x) for x in obtained_gates[0].parameters] + + print(f"Expected = {expected_params}") + print(f"Obtained = {obtained_params}") + + assert isinstance(obtained_gates[0], expected_gate) + assert obtained_gates[0].wires == Wires("a") + assert qml.math.allclose(obtained_params, expected_params, atol=1e-7) From 3fef91dbd413f75f5077bd60aa730ed0ff91c255 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 15:03:32 -0400 Subject: [PATCH 07/37] Change check for diagonal to use matrix element instead of computed angle. --- pennylane/transforms/decompositions.py | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 53f96b383a6..2c53b7bbaf7 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -19,7 +19,7 @@ from pennylane.transforms import qfunc_transform -def _convert_to_su2(U, zero_tol=1e-6): +def _convert_to_su2(U): r"""Check unitarity of a matrix and convert it to :math:`SU(2)` if possible. Args: @@ -35,7 +35,7 @@ def _convert_to_su2(U, zero_tol=1e-6): # Check unitarity if not qml.math.allclose( - qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=zero_tol + qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=1e-7 ): raise ValueError("Operator must be unitary.") @@ -43,14 +43,14 @@ def _convert_to_su2(U, zero_tol=1e-6): 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 qml.math.allclose(det, [1.0], atol=zero_tol): + if not qml.math.allclose(det, [1.0]): exp_angle = -1j * qml.math.cast_like(qml.math.angle(det), 1j) / 2 U = U * (qml.math.exp(exp_angle)) return U -def _zyz_decomposition(U, wire, zero_tol=1e-6): +def _zyz_decomposition(U, wire): r"""Helper function to recover the rotation angles of a single-qubit matrix :math:`U`. The set of angles are chosen so as to implement :math:`U` up to a global phase. @@ -64,22 +64,22 @@ def _zyz_decomposition(U, wire, zero_tol=1e-6): (float, float, float): A set of angles (:math:`\phi`, :math:`\theta`, :math:`\omega`) that implement U as a sequence :math:`U = RZ(\omega) RY(\theta) RZ(\phi)`. """ - U = _convert_to_su2(U, zero_tol) + U = _convert_to_su2(U) - # Compute the angle of the RY - cos2_theta_over_2 = qml.math.abs(U[0, 0] * U[1, 1]) - theta = 2 * qml.math.arccos(qml.math.sqrt(cos2_theta_over_2)) - - # If it's close to 0, the matrix is diagonal and we have just an RZ rotation - if qml.math.allclose(theta, [0.0], atol=zero_tol): + # 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 qml.math.allclose(U[0, 1], [0.0]): omega = 2 * qml.math.angle(U[1, 1]) return [qml.RZ(omega, wires=wire)] - # Otherwise recover the decomposition as a Rot, which can be further decomposed - # if desired. 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. - if qml.math.allclose(U[0, 0], [0.0], atol=zero_tol): + # If not diagonal, compute the angle of the RY + cos2_theta_over_2 = qml.math.abs(U[0, 0] * U[1, 1]) + theta = 2 * qml.math.arccos(qml.math.sqrt(cos2_theta_over_2)) + + # 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 qml.math.allclose(U[0, 0], [0.0]): phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) omega = -phi - qml.math.cast_like(2 * qml.math.angle(U[1, 0]), phi) else: From 4d6699752c7b68033bd3976a22a4e04d10d7c29a Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 15:08:31 -0400 Subject: [PATCH 08/37] Get things working for 64 bit data in all interfaces. --- pennylane/transforms/decompositions.py | 4 ++-- tests/transforms/test_decompositions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 2c53b7bbaf7..98a80a7673d 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -35,7 +35,7 @@ def _convert_to_su2(U): # Check unitarity if not qml.math.allclose( - qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=1e-7 + qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=1e-7 ): raise ValueError("Operator must be unitary.") @@ -45,7 +45,7 @@ def _convert_to_su2(U): # Convert to SU(2) if it's not close to 1 if not qml.math.allclose(det, [1.0]): exp_angle = -1j * qml.math.cast_like(qml.math.angle(det), 1j) / 2 - U = U * (qml.math.exp(exp_angle)) + U = qml.math.cast_like(U, exp_angle) * qml.math.exp(exp_angle) return U diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index ad010403181..67845a13b54 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -65,7 +65,7 @@ def test_zyz_decomposition_torch(self, U, expected_gate, expected_params): """Test that a one-qubit operation in Torch is correctly decomposed.""" torch = pytest.importorskip("torch") - U = torch.tensor(U, dtype=torch.complex128) + U = torch.tensor(U, dtype=torch.complex64) obtained_gates = _zyz_decomposition(U, wire="a") @@ -82,7 +82,7 @@ def test_zyz_decomposition_tf(self, U, expected_gate, expected_params): """Test that a one-qubit operation in Tensorflow is correctly decomposed.""" tf = pytest.importorskip("tensorflow") - U = tf.Variable(U, dtype=tf.complex128) + U = tf.Variable(U, dtype=tf.complex64) obtained_gates = _zyz_decomposition(U, wire="a") @@ -113,4 +113,4 @@ def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): assert isinstance(obtained_gates[0], expected_gate) assert obtained_gates[0].wires == Wires("a") - assert qml.math.allclose(obtained_params, expected_params, atol=1e-7) + assert qml.math.allclose(obtained_params, expected_params) From 5f723fb1ffe79c49965222aeab0f9b57257b1ea1 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 15:10:34 -0400 Subject: [PATCH 09/37] Remove print statements. --- tests/transforms/test_decompositions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index 67845a13b54..97b9c949917 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -41,7 +41,6 @@ (np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix, qml.Rot, [-1.0, 2.0, -3.0]), ] - class TestQubitUnitaryDecompositionHelpers: """Test that the decompsoitions are correct.""" @@ -108,9 +107,6 @@ def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): obtained_params = [jnp.asarray(x) for x in obtained_gates[0].parameters] - print(f"Expected = {expected_params}") - print(f"Obtained = {obtained_params}") - assert isinstance(obtained_gates[0], expected_gate) assert obtained_gates[0].wires == Wires("a") assert qml.math.allclose(obtained_params, expected_params) From b8a524b6693796911f0087e489bbad6fd71882c9 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 15:26:47 -0400 Subject: [PATCH 10/37] Add test of transform in all interfaces. --- pennylane/transforms/decompositions.py | 2 +- tests/transforms/test_decompositions.py | 119 +++++++++++++++++++++--- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 98a80a7673d..40e8d09fba3 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -106,7 +106,7 @@ def decompose_single_qubit_unitaries(tape): if dim_U != 2: continue - decomp = _zyz_decomposition(op.parameters[0], op.wire) + decomp = _zyz_decomposition(op.parameters[0], op.wires[0]) for d_op in decomp: d_op.queue() diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index 97b9c949917..758b802eba4 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -41,7 +41,14 @@ (np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix, qml.Rot, [-1.0, 2.0, -3.0]), ] -class TestQubitUnitaryDecompositionHelpers: +# A simple quantum function for testing +def qfunc(U): + qml.Hadamard(wires="a") + qml.QubitUnitary(U, wires="a") + qml.CNOT(wires=["b", "a"]) + + +class TestQubitUnitaryZYZDecomposition: """Test that the decompsoitions are correct.""" def test_zyz_decomposition_invalid_input(self): @@ -69,12 +76,11 @@ def test_zyz_decomposition_torch(self, U, expected_gate, expected_params): obtained_gates = _zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 - - obtained_params = [x.detach() for x in obtained_gates[0].parameters] - assert isinstance(obtained_gates[0], expected_gate) assert obtained_gates[0].wires == Wires("a") - assert qml.math.allclose(obtained_params, expected_params) + assert qml.math.allclose( + [x.detach() for x in obtained_gates[0].parameters], expected_params + ) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) def test_zyz_decomposition_tf(self, U, expected_gate, expected_params): @@ -86,27 +92,110 @@ def test_zyz_decomposition_tf(self, U, expected_gate, expected_params): obtained_gates = _zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 - - obtained_params = [x.numpy() for x in obtained_gates[0].parameters] - assert isinstance(obtained_gates[0], expected_gate) assert obtained_gates[0].wires == Wires("a") - assert qml.math.allclose(obtained_params, expected_params) + assert qml.math.allclose([x.numpy() for x in obtained_gates[0].parameters], expected_params) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): """Test that a one-qubit operation in JAX is correctly decomposed.""" jax = pytest.importorskip("jax") - from jax import numpy as jnp - U = jnp.array(U, dtype=jnp.complex64) + U = jax.numpy.array(U, dtype=jax.numpy.complex64) obtained_gates = _zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 - - obtained_params = [jnp.asarray(x) for x in obtained_gates[0].parameters] - assert isinstance(obtained_gates[0], expected_gate) assert obtained_gates[0].wires == Wires("a") - assert qml.math.allclose(obtained_params, expected_params) + assert qml.math.allclose( + [jax.numpy.asarray(x) for x in obtained_gates[0].parameters], expected_params + ) + + +class TestDecomposeSingleQubitUnitary: + """Tests to ensure the transform itself works in all interfaces.""" + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries(self, U, expected_gate, expected_params): + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose(ops[1].parameters, expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_torch(self, U, expected_gate, expected_params): + torch = pytest.importorskip("torch") + + U = torch.tensor(U, dtype=torch.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([x.detach() for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_tf(self, U, expected_gate, expected_params): + tf = pytest.importorskip("tensorflow") + + U = tf.Variable(U, dtype=tf.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([x.numpy() for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_jax(self, U, expected_gate, expected_params): + jax = pytest.importorskip("jax") + + U = jax.numpy.array(U, dtype=jax.numpy.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([jax.numpy.asarray(x) for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) From 60a0b1efaf915e6f5a110a776fdce3d2b5dddea9 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Thu, 24 Jun 2021 16:42:10 -0400 Subject: [PATCH 11/37] Differentiability tests (currently only working for autograd). --- tests/transforms/test_decompositions.py | 177 +++++++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index 758b802eba4..4a4fb57c78a 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -113,7 +113,7 @@ def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): ) -class TestDecomposeSingleQubitUnitary: +class TestDecomposeSingleQubitUnitaryTransform: """Tests to ensure the transform itself works in all interfaces.""" @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) @@ -199,3 +199,178 @@ def test_decompose_single_qubit_unitaries_jax(self, U, expected_gate, expected_p assert isinstance(ops[2], qml.CNOT) assert ops[2].wires == Wires(["b", "a"]) + + +# A simple circuit; we will test QubitUnitary on matrices constructed using trainable +# parameters, and RZ/RX are easy to write the matrices for. +def original_qfunc_for_grad(angles): + qml.Hadamard(wires="a") + qml.RZ(angles[0], wires="a") + qml.RX(angles[1], wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + +dev = qml.device("default.qubit", wires=["a", "b"]) + +angle_pairs = [(0.3, 0.3), (np.pi, -0.65), (0.0, np.pi / 2), (np.pi / 3, 0.0)] + + +class TestQubitUnitaryDifferentiability: + """Tests to ensure the transform is fully differentiable in all interfaces.""" + + @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) + def test_gradient_qubit_unitary(self, x_rot, z_rot): + """Tests differentiability in autograd interface.""" + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + Z_mat = np.array([[qml.math.exp(-1j * z / 2), 0], [0, qml.math.exp(1j * z / 2)]]) + X_mat = np.array( + [ + [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], + [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + ] + ) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev) + transformed_qnode = qml.QNode(transformed_qfunc, dev) + + input = np.array([x_rot, z_rot], requires_grad=True) + assert qml.math.allclose(original_qnode(input), transformed_qnode(input)) + + original_grad = qml.grad(original_qnode)(input) + transformed_grad = qml.grad(transformed_qnode)(input) + assert qml.math.allclose(original_grad, transformed_grad) + + @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) + def test_gradient_qubit_unitary_torch(self, x_rot, z_rot): + """Tests differentiability in torch interface.""" + torch = pytest.importorskip("torch") + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + Z_mat = torch.tensor( + [[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]] + ) + X_mat = torch.tensor( + [ + [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], + [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + ] + ) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliZ(wires="a")) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") + + original_input = torch.tensor([x_rot, z_rot], requires_grad=True) + original_result = original_qnode(original_input) + transformed_input = torch.tensor([x_rot, z_rot], requires_grad=True) + transformed_result = transformed_qnode(transformed_input) + assert qml.math.allclose(original_result, transformed_result) + + original_result.backward() + transformed_result.backward() + + assert qml.math.allclose(original_result.grad, transformed_result.grad) + + @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_tf(self, x_rot, z_rot): + """Tests differentiability in tensorflow interface.""" + tf = pytest.importorskip("tensorflow") + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + Z_mat = tf.Variable([[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]]) + X_mat = tf.Variable( + [ + [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], + [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + ] + ) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") + + original_input = tf.Variable([x_rot, z_rot]) + original_result = original_qnode(original_input) + transformed_input = tf.Variable([x_rot, z_rot]) + transformed_result = transformed_qnode(transformed_input) + assert qml.math.allclose(original_result, transformed_result) + + with tf.GradientTape() as tape: + loss = original_qnode(original_input) + original_grad = tape.gradient(loss, original_input) + + with tf.GradientTape() as tape: + loss = transformed_qnode(transformed_input) + transformed_grad = tape.gradient(loss, transformed_input) + + assert qml.math.allclose(original_grad, transformed_grad) + + @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_jax(self, x_rot, z_rot): + """Tests differentiability in jax interface.""" + jax = pytest.importorskip("jax") + from jax import numpy as jnp + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + Z_mat = jnp.array( + [[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]], + ) + X_mat = jnp.array( + [ + [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], + [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + ], + ) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") + + original_input = jnp.array([x_rot, z_rot], dtype=jnp.complex64) + original_result = original_qnode(original_input) + transformed_input = jnp.array([x_rot, z_rot], dtype=jnp.complex64) + transformed_result = transformed_qnode(transformed_input) + assert qml.math.allclose(original_result, transformed_result) + + original_grad = jax.grad(original_qnode)(original_input) + transformed_grad = jax.grad(transformed_qnode)(transformed_input) + assert qml.math.allclose(original_grad, transformed_grad) From f8d41ceebec947176525de69b04de57ae3396566 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Fri, 25 Jun 2021 09:56:06 -0400 Subject: [PATCH 12/37] Fix order of variables and use interface math for each case. --- tests/transforms/test_decompositions.py | 79 +++++++++++++------------ 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index 4a4fb57c78a..c602dbe8457 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -226,11 +226,11 @@ def test_gradient_qubit_unitary(self, x_rot, z_rot): def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] - Z_mat = np.array([[qml.math.exp(-1j * z / 2), 0], [0, qml.math.exp(1j * z / 2)]]) + Z_mat = np.array([[np.exp(-1j * z / 2), 0], [0, np.exp(1j * z / 2)]]) X_mat = np.array( [ - [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], - [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + [np.cos(x / 2), -1j * np.sin(x / 2)], + [-1j * np.sin(x / 2), np.cos(x / 2)], ] ) @@ -240,16 +240,17 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - original_qnode = qml.QNode(original_qfunc_for_grad, dev) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev) - input = np.array([x_rot, z_rot], requires_grad=True) + input = np.array([z_rot, x_rot], requires_grad=True) assert qml.math.allclose(original_qnode(input), transformed_qnode(input)) original_grad = qml.grad(original_qnode)(input) transformed_grad = qml.grad(transformed_qnode)(input) + assert qml.math.allclose(original_grad, transformed_grad) @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) @@ -261,36 +262,37 @@ def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] Z_mat = torch.tensor( - [[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]] + [[torch.exp(-1j * z / 2), 0.0], [0.0, torch.exp(1j * z / 2)]], requires_grad=True ) X_mat = torch.tensor( [ - [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], - [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], - ] + [torch.cos(x / 2), -1j * torch.sin(x / 2)], + [-1j * torch.sin(x / 2), torch.cos(x / 2)], + ], + requires_grad=True, ) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") qml.QubitUnitary(X_mat, wires="b") qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliZ(wires="a")) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + return qml.expval(qml.PauliX(wires="a")) original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") - - original_input = torch.tensor([x_rot, z_rot], requires_grad=True) + original_input = torch.tensor([z_rot, x_rot], requires_grad=True) original_result = original_qnode(original_input) - transformed_input = torch.tensor([x_rot, z_rot], requires_grad=True) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") + transformed_input = torch.tensor([z_rot, x_rot], requires_grad=True) transformed_result = transformed_qnode(transformed_input) + assert qml.math.allclose(original_result, transformed_result) original_result.backward() transformed_result.backward() - assert qml.math.allclose(original_result.grad, transformed_result.grad) + assert qml.math.allclose(original_input.grad, transformed_input.grad) @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) def test_decompose_single_qubit_unitaries_tf(self, x_rot, z_rot): @@ -300,11 +302,11 @@ def test_decompose_single_qubit_unitaries_tf(self, x_rot, z_rot): def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] - Z_mat = tf.Variable([[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]]) + Z_mat = tf.Variable([[tf.math.exp(-1j * z / 2), 0.0], [0.0, tf.math.exp(1j * z / 2)]]) X_mat = tf.Variable( [ - [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], - [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + [tf.math.cos(x / 2), -1j * tf.math.sin(x / 2)], + [-1j * tf.math.sin(x / 2), tf.math.cos(x / 2)], ] ) @@ -314,15 +316,15 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") - - original_input = tf.Variable([x_rot, z_rot]) + original_input = tf.Variable([z_rot, x_rot], dtype=tf.complex64) original_result = original_qnode(original_input) - transformed_input = tf.Variable([x_rot, z_rot]) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") + transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.complex64) transformed_result = transformed_qnode(transformed_input) + assert qml.math.allclose(original_result, transformed_result) with tf.GradientTape() as tape: @@ -345,12 +347,12 @@ def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] Z_mat = jnp.array( - [[qml.math.exp(-1j * z / 2), 0.0], [0.0, qml.math.exp(1j * z / 2)]], + [[jnp.exp(-1j * z / 2), 0.0], [0.0, jnp.exp(1j * z / 2)]], ) X_mat = jnp.array( [ - [qml.math.cos(x / 2), -1j * qml.math.sin(x / 2)], - [-1j * qml.math.sin(x / 2), qml.math.cos(x / 2)], + [jnp.cos(x / 2), -1j * jnp.sin(x / 2)], + [-1j * jnp.sin(x / 2), jnp.cos(x / 2)], ], ) @@ -360,17 +362,16 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + input = jnp.array([z_rot, x_rot], dtype=jnp.complex64) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax", diff_method="backprop") + original_result = original_qnode(input) - original_input = jnp.array([x_rot, z_rot], dtype=jnp.complex64) - original_result = original_qnode(original_input) - transformed_input = jnp.array([x_rot, z_rot], dtype=jnp.complex64) - transformed_result = transformed_qnode(transformed_input) + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax", diff_method="backprop") + transformed_result = transformed_qnode(input) assert qml.math.allclose(original_result, transformed_result) - original_grad = jax.grad(original_qnode)(original_input) - transformed_grad = jax.grad(transformed_qnode)(transformed_input) + original_grad = jax.grad(original_qnode)(input) + transformed_grad = jax.grad(transformed_qnode)(input) assert qml.math.allclose(original_grad, transformed_grad) From dccdba7116b156bc83492399647c744d5afcf2f6 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Fri, 25 Jun 2021 10:39:09 -0400 Subject: [PATCH 13/37] Fix return of single measurement in qfunc transform. --- pennylane/transforms/qfunc_transforms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pennylane/transforms/qfunc_transforms.py b/pennylane/transforms/qfunc_transforms.py index 9581bae67aa..13263af3075 100644 --- a/pennylane/transforms/qfunc_transforms.py +++ b/pennylane/transforms/qfunc_transforms.py @@ -182,6 +182,10 @@ def _create_qfunc_internal_wrapper(fn, tape_transform, transform_args, transform def internal_wrapper(*args, **kwargs): tape = make_tape(fn)(*args, **kwargs) tape = tape_transform(tape, *transform_args, **transform_kwargs) + + if len(tape.measurements) == 1: + return tape.measurements[0] + return tape.measurements return internal_wrapper From f45734724769b52ce03b1e95558a9746c0683b73 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Fri, 25 Jun 2021 10:51:07 -0400 Subject: [PATCH 14/37] Add unit test. --- tests/transforms/test_qfunc_transform.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/transforms/test_qfunc_transform.py b/tests/transforms/test_qfunc_transform.py index f7d157de5ae..066a24f3889 100644 --- a/tests/transforms/test_qfunc_transform.py +++ b/tests/transforms/test_qfunc_transform.py @@ -271,6 +271,37 @@ def ansatz(): assert ops[0].parameters == [x] assert ops[1].name == "CZ" + def test_transform_single_measurement(self): + """Test that transformed functions return a scalar value when there is only + a single measurement.""" + + @qml.qfunc_transform + def expand_hadamards(tape): + for op in tape.operations + tape.measurements: + if op.name == "Hadamard": + qml.RZ(np.pi, wires=op.wires) + qml.RY(np.pi / 2, wires=op.wires) + else: + op.queue() + + def ansatz(): + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliX(wires=1)) + + dev = qml.device("default.qubit", wires=2) + + normal_qnode = qml.QNode(ansatz, dev) + + transformed_ansatz = expand_hadamards(ansatz) + transformed_qnode = qml.QNode(transformed_ansatz, dev) + + normal_result = normal_qnode() + transformed_result = transformed_qnode() + + assert np.allclose(normal_result, transformed_result) + assert normal_result.shape == transformed_result.shape + ############################################ # Test transform, ansatz, and qfunc function From 284e65fe30454fa2c37a8a5067969ebd8a301a88 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Fri, 25 Jun 2021 10:52:59 -0400 Subject: [PATCH 15/37] Update CHANGELOG. --- .github/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 30b4da82720..8bbec02dd39 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -11,13 +11,17 @@

Bug fixes

+* Quantum function transforms now preserve the format of the measurement + results, so that a single measurement returns a single value rather than + an array with a single element. [(#1434)](https://github.com/PennyLaneAI/pennylane/pull/1434/files) +

Documentation

Contributors

This release contains contributions from (in alphabetical order): -Ashish Panigrahi +Olivia Di Matteo, Ashish Panigrahi # Release 0.16.0 (current release) From c6ad65b94c295d6df5b4378b2fd27883f641aa4a Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Fri, 25 Jun 2021 13:18:47 -0400 Subject: [PATCH 16/37] Coax all the interfaces to compute gradients. --- tests/transforms/test_decompositions.py | 105 ++++++++++++------------ 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index c602dbe8457..55e089f6de2 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -219,20 +219,19 @@ def original_qfunc_for_grad(angles): class TestQubitUnitaryDifferentiability: """Tests to ensure the transform is fully differentiable in all interfaces.""" - @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) - def test_gradient_qubit_unitary(self, x_rot, z_rot): + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_gradient_qubit_unitary(self, z_rot, x_rot): """Tests differentiability in autograd interface.""" def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] - Z_mat = np.array([[np.exp(-1j * z / 2), 0], [0, np.exp(1j * z / 2)]]) - X_mat = np.array( - [ - [np.cos(x / 2), -1j * np.sin(x / 2)], - [-1j * np.sin(x / 2), np.cos(x / 2)], - ] - ) + + Z_mat = np.array([[np.exp(-1j * z / 2), 0.0], [0.0, np.exp(1j * z / 2)]]) + + c = np.cos(x / 2) + s = np.sin(x / 2) * 1j + X_mat = np.array([[c, -s], [-s, c]]) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") @@ -253,24 +252,30 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_grad, transformed_grad) - @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) - def test_gradient_qubit_unitary_torch(self, x_rot, z_rot): + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_gradient_qubit_unitary_torch(self, z_rot, x_rot): """Tests differentiability in torch interface.""" torch = pytest.importorskip("torch") def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] - Z_mat = torch.tensor( - [[torch.exp(-1j * z / 2), 0.0], [0.0, torch.exp(1j * z / 2)]], requires_grad=True - ) - X_mat = torch.tensor( + + # Had to do this in order to make a torch tensor of torch tensors + Z_mat = torch.stack( [ - [torch.cos(x / 2), -1j * torch.sin(x / 2)], - [-1j * torch.sin(x / 2), torch.cos(x / 2)], - ], - requires_grad=True, - ) + torch.exp(-1j * z / 2), + torch.tensor(0.0), + torch.tensor(0.0), + torch.exp(1j * z / 2), + ] + ).reshape(2, 2) + + # Variables need to be complex + c = torch.cos(x / 2).type(torch.complex64) + s = torch.sin(x / 2) * 1j + + X_mat = torch.stack([c, -s, -s, c]).reshape(2, 2) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") @@ -279,12 +284,12 @@ def qfunc_with_qubit_unitary(angles): return qml.expval(qml.PauliX(wires="a")) original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") - original_input = torch.tensor([z_rot, x_rot], requires_grad=True) + original_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) original_result = original_qnode(original_input) transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") - transformed_input = torch.tensor([z_rot, x_rot], requires_grad=True) + transformed_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) transformed_result = transformed_qnode(transformed_input) assert qml.math.allclose(original_result, transformed_result) @@ -294,21 +299,20 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_input.grad, transformed_input.grad) - @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_tf(self, x_rot, z_rot): + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_tf(self, z_rot, x_rot): """Tests differentiability in tensorflow interface.""" tf = pytest.importorskip("tensorflow") def qfunc_with_qubit_unitary(angles): - z = angles[0] - x = angles[1] - Z_mat = tf.Variable([[tf.math.exp(-1j * z / 2), 0.0], [0.0, tf.math.exp(1j * z / 2)]]) - X_mat = tf.Variable( - [ - [tf.math.cos(x / 2), -1j * tf.math.sin(x / 2)], - [-1j * tf.math.sin(x / 2), tf.math.cos(x / 2)], - ] - ) + z = tf.cast(angles[0], tf.complex64) + x = tf.cast(angles[1], tf.complex64) + + c = tf.cos(x / 2) + s = tf.sin(x / 2) * 1j + + Z_mat = tf.convert_to_tensor([[tf.exp(-1j * z / 2), 0.0], [0.0, tf.exp(1j * z / 2)]]) + X_mat = tf.convert_to_tensor([[c, -s], [-s, c]]) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") @@ -317,12 +321,12 @@ def qfunc_with_qubit_unitary(angles): return qml.expval(qml.PauliX(wires="a")) original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") - original_input = tf.Variable([z_rot, x_rot], dtype=tf.complex64) + original_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) original_result = original_qnode(original_input) transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") - transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.complex64) + transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) transformed_result = transformed_qnode(transformed_input) assert qml.math.allclose(original_result, transformed_result) @@ -335,10 +339,11 @@ def qfunc_with_qubit_unitary(angles): loss = transformed_qnode(transformed_input) transformed_grad = tape.gradient(loss, transformed_input) - assert qml.math.allclose(original_grad, transformed_grad) + # For 64bit values, need to slightly increase the tolerance threshold + assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) - @pytest.mark.parametrize("x_rot,z_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_jax(self, x_rot, z_rot): + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_jax(self, z_rot, x_rot): """Tests differentiability in jax interface.""" jax = pytest.importorskip("jax") from jax import numpy as jnp @@ -346,15 +351,12 @@ def test_decompose_single_qubit_unitaries_jax(self, x_rot, z_rot): def qfunc_with_qubit_unitary(angles): z = angles[0] x = angles[1] - Z_mat = jnp.array( - [[jnp.exp(-1j * z / 2), 0.0], [0.0, jnp.exp(1j * z / 2)]], - ) - X_mat = jnp.array( - [ - [jnp.cos(x / 2), -1j * jnp.sin(x / 2)], - [-1j * jnp.sin(x / 2), jnp.cos(x / 2)], - ], - ) + + Z_mat = jnp.array([[jnp.exp(-1j * z / 2), 0.0], [0.0, jnp.exp(1j * z / 2)]]) + + c = jnp.cos(x / 2) + s = jnp.sin(x / 2) * 1j + X_mat = jnp.array([[c, -s], [-s, c]]) qml.Hadamard(wires="a") qml.QubitUnitary(Z_mat, wires="a") @@ -362,16 +364,17 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - input = jnp.array([z_rot, x_rot], dtype=jnp.complex64) + # Setting the dtype to complex64 causes the gradients to be complex... + input = jnp.array([z_rot, x_rot], dtype=jnp.float64) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax", diff_method="backprop") + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") original_result = original_qnode(input) transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax", diff_method="backprop") + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") transformed_result = transformed_qnode(input) assert qml.math.allclose(original_result, transformed_result) original_grad = jax.grad(original_qnode)(input) transformed_grad = jax.grad(transformed_qnode)(input) - assert qml.math.allclose(original_grad, transformed_grad) + assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) From 5fddce4ad71e4537a83683b28d3448c124e9653a Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 08:28:55 -0400 Subject: [PATCH 17/37] Add comment for circular import. --- pennylane/transforms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index e797bdfe417..e15022d55d3 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -75,6 +75,7 @@ ~qfunc_transform ~transforms.make_tape """ +# Import the decorators first to prevent circular imports when used in other transforms from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform from .adjoint import adjoint from .classical_jacobian import classical_jacobian From e4591c963264015984bdeb9efb1d10a6ca5db27c Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com> Date: Tue, 29 Jun 2021 08:31:11 -0400 Subject: [PATCH 18/37] Apply suggestions from code review Co-authored-by: Josh Izaac --- pennylane/transforms/decompositions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 40e8d09fba3..045cd83b165 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -15,7 +15,6 @@ Contains transforms for decomposing arbitrary unitary operations into elementary gates. """ import pennylane as qml -import numpy as np from pennylane.transforms import qfunc_transform From 0fe201e56a22133fe856b56970d35c3aa6415720 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 08:33:36 -0400 Subject: [PATCH 19/37] Fix shape check in convert_to_su2. --- pennylane/transforms/decompositions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 045cd83b165..90281f9dc52 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -28,9 +28,11 @@ def _convert_to_su2(U): array[complex]: A :math:`2 \times 2` matrix in :math:`SU(2)` that is equivalent to U up to a global phase. """ + shape = qml.math.shape(U) + # Check dimensions - if qml.math.shape(U)[0] != 2 or qml.math.shape(U)[1] != 2: - raise ValueError("Cannot convert matrix with shape {qml.math.shape(U)} to SU(2).") + if shape != (2, 2): + raise ValueError(f"Cannot convert matrix with shape {shape} to SU(2).") # Check unitarity if not qml.math.allclose( From b09e6d86ef93afa2e9bacc2ef72784ee0ff43993 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 08:35:55 -0400 Subject: [PATCH 20/37] Change qml.math to math. --- pennylane/transforms/decompositions.py | 37 +++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions.py index 90281f9dc52..35e944e48f4 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions.py @@ -15,6 +15,7 @@ Contains transforms for decomposing arbitrary unitary operations into elementary gates. """ import pennylane as qml +from pennylane import math from pennylane.transforms import qfunc_transform @@ -28,15 +29,15 @@ def _convert_to_su2(U): array[complex]: A :math:`2 \times 2` matrix in :math:`SU(2)` that is equivalent to U up to a global phase. """ - shape = qml.math.shape(U) + shape = math.shape(U) # Check dimensions if shape != (2, 2): raise ValueError(f"Cannot convert matrix with shape {shape} to SU(2).") # Check unitarity - if not qml.math.allclose( - qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(2), atol=1e-7 + if not math.allclose( + math.dot(U, math.T(math.conj(U))), math.eye(2), atol=1e-7 ): raise ValueError("Operator must be unitary.") @@ -44,9 +45,9 @@ def _convert_to_su2(U): 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 qml.math.allclose(det, [1.0]): - exp_angle = -1j * qml.math.cast_like(qml.math.angle(det), 1j) / 2 - U = qml.math.cast_like(U, exp_angle) * qml.math.exp(exp_angle) + 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 @@ -69,27 +70,27 @@ def _zyz_decomposition(U, wire): # 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 qml.math.allclose(U[0, 1], [0.0]): - omega = 2 * qml.math.angle(U[1, 1]) + if math.allclose(U[0, 1], [0.0]): + omega = 2 * math.angle(U[1, 1]) return [qml.RZ(omega, wires=wire)] # If not diagonal, compute the angle of the RY - cos2_theta_over_2 = qml.math.abs(U[0, 0] * U[1, 1]) - theta = 2 * qml.math.arccos(qml.math.sqrt(cos2_theta_over_2)) + cos2_theta_over_2 = math.abs(U[0, 0] * U[1, 1]) + theta = 2 * math.arccos(math.sqrt(cos2_theta_over_2)) # 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 qml.math.allclose(U[0, 0], [0.0]): - phi = 1j * qml.math.log(U[0, 1] / U[1, 0]) - omega = -phi - qml.math.cast_like(2 * qml.math.angle(U[1, 0]), phi) + if math.allclose(U[0, 0], [0.0]): + phi = 1j * math.log(U[0, 1] / U[1, 0]) + omega = -phi - math.cast_like(2 * math.angle(U[1, 0]), phi) else: el_division = U[0, 0] / U[1, 0] - tan_part = qml.math.cast_like(qml.math.tan(theta / 2), el_division) - omega = 1j * qml.math.log(tan_part * el_division) - phi = -omega - qml.math.cast_like(2 * qml.math.angle(U[0, 0]), omega) + 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(qml.math.real(phi), qml.math.real(theta), qml.math.real(omega), wires=wire)] + return [qml.Rot(math.real(phi), math.real(theta), math.real(omega), wires=wire)] @qfunc_transform @@ -102,7 +103,7 @@ def decompose_single_qubit_unitaries(tape): """ for op in tape.operations + tape.measurements: if isinstance(op, qml.QubitUnitary): - dim_U = qml.math.shape(op.parameters[0])[0] + dim_U = math.shape(op.parameters[0])[0] if dim_U != 2: continue From 91ae53638dc7e7f7ebb39995ad2da49b77b6f8bf Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 09:45:07 -0400 Subject: [PATCH 21/37] Restructure; move decompositions into new directory. --- pennylane/ops/qubit.py | 2 +- pennylane/transforms/__init__.py | 4 +- .../decompose_single_qubit_unitaries.py | 87 +++++ .../transforms/decompositions/__init__.py | 18 + .../single_qubit_unitary.py} | 67 ++-- .../test_decompose_single_qubit_unitaries.py | 318 ++++++++++++++++++ tests/transforms/test_decompositions.py | 289 +--------------- 7 files changed, 466 insertions(+), 319 deletions(-) create mode 100644 pennylane/transforms/decompose_single_qubit_unitaries.py create mode 100644 pennylane/transforms/decompositions/__init__.py rename pennylane/transforms/{decompositions.py => decompositions/single_qubit_unitary.py} (65%) create mode 100644 tests/transforms/test_decompose_single_qubit_unitaries.py diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index f9dca4f4b3d..90b598e32ac 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2162,7 +2162,7 @@ def decomposition(U, wires): # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ if qml.math.shape(U)[0] == 2: wire = Wires(wires)[0] - decomp_ops = qml.transforms.decompositions._zyz_decomposition(U, wire) + decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire) return decomp_ops return NotImplementedError("Decompositions only supported for single-qubit unitaries") diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index e15022d55d3..232baea8417 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -45,6 +45,7 @@ ~adjoint ~ctrl + ~transforms.decompose_single_qubit_unitaries ~transforms.invisible ~apply_controlled_Q ~quantum_monte_carlo @@ -80,7 +81,8 @@ from .adjoint import adjoint from .classical_jacobian import classical_jacobian from .control import ControlledOperation, ctrl -from .decompositions import decompose_single_qubit_unitaries +from .decompositions import zyz_decomposition +from .decompose_single_qubit_unitaries import decompose_single_qubit_unitaries from .draw import draw from .hamiltonian_expand import hamiltonian_expand from .invisible import invisible diff --git a/pennylane/transforms/decompose_single_qubit_unitaries.py b/pennylane/transforms/decompose_single_qubit_unitaries.py new file mode 100644 index 00000000000..e641fd6bfdf --- /dev/null +++ b/pennylane/transforms/decompose_single_qubit_unitaries.py @@ -0,0 +1,87 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +A transform for decomposing arbitrary single-qubit QubitUnitary gates into elementary gates. +""" +import pennylane as qml +from pennylane import math +from pennylane.transforms import qfunc_transform +from pennylane.transforms.decompositions import zyz_decomposition + + +@qfunc_transform +def decompose_single_qubit_unitaries(tape): + """Quantum function transform to decomposes all instances of single-qubit QubitUnitary + operations to parametrized single-qubit operations. + + Diagonal operations will be converted to a single ``RZ`` gate, while non-diagonal + operations will be converted to a ``Rot`` gate that implements the original operation + up to a global phase. + + Args: + tape (qml.tape.QuantumTape): A quantum tape. + + **Example** + + Suppose we would like to apply the following unitary operation: + + .. code-block:: python3 + + U = np.array([ + [-0.17111489+0.58564875j, -0.69352236-0.38309524j], + [ 0.25053735+0.75164238j, 0.60700543-0.06171855j] + ]) + + The ``decompose_single_qubit_unitaries`` transform enables us to decompose + such numerical operations (as well as unitaries that may be defined by parameters + within the QNode, and instantiated therein), while preserving differentiability. + + + .. code-block:: python3 + + def qfunc(): + qml.QubitUnitary(U, wires=0) + return qml.expval(qml.PauliZ(0) + + The original circuit is: + + >>> dev = qml.device('default.qubit', wires=1) + >>> qnode = qml.QNode(qfunc, dev) + >>> print(qml.draw(qnode)()) + 0: ──U0──┤ ⟨Z⟩ + U0 = + [[-0.17111489+0.58564875j -0.69352236-0.38309524j] + [ 0.25053735+0.75164238j 0.60700543-0.06171855j]] + + We can use the transform to decompose the gate: + + >>> transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + >>> transformed_qnode = qml.QNode(transformed_qfunc, dev) + >>> print(qml.draw(transformed_qnode)()) + 0: ──Rot(-1.35, 1.83, -0.606)──┤ ⟨Z⟩ + + """ + for op in tape.operations + tape.measurements: + if isinstance(op, qml.QubitUnitary): + dim_U = math.shape(op.parameters[0])[0] + + if dim_U != 2: + continue + + decomp = zyz_decomposition(op.parameters[0], op.wires[0]) + + for d_op in decomp: + d_op.queue() + else: + op.queue() diff --git a/pennylane/transforms/decompositions/__init__.py b/pennylane/transforms/decompositions/__init__.py new file mode 100644 index 00000000000..8e771a31d1f --- /dev/null +++ b/pennylane/transforms/decompositions/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r"""This module contains decompositions for (numerically-specified) arbitrary +unitary operations into sequences of elementary operations. +""" + +from .single_qubit_unitary import zyz_decomposition diff --git a/pennylane/transforms/decompositions.py b/pennylane/transforms/decompositions/single_qubit_unitary.py similarity index 65% rename from pennylane/transforms/decompositions.py rename to pennylane/transforms/decompositions/single_qubit_unitary.py index 35e944e48f4..7eb14e4c0df 100644 --- a/pennylane/transforms/decompositions.py +++ b/pennylane/transforms/decompositions/single_qubit_unitary.py @@ -11,12 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -Contains transforms for decomposing arbitrary unitary operations into elementary gates. +"""Contains transforms and helpers functions for decomposing arbitrary unitary +operations into elementary gates. """ import pennylane as qml from pennylane import math -from pennylane.transforms import qfunc_transform def _convert_to_su2(U): @@ -36,9 +35,7 @@ def _convert_to_su2(U): raise ValueError(f"Cannot convert matrix with shape {shape} to SU(2).") # Check unitarity - if not math.allclose( - math.dot(U, math.T(math.conj(U))), math.eye(2), atol=1e-7 - ): + 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 @@ -52,19 +49,40 @@ def _convert_to_su2(U): return U -def _zyz_decomposition(U, wire): - r"""Helper function to recover the rotation angles of a single-qubit matrix :math:`U`. +def zyz_decomposition(U, wire): + r"""Recover the decomposition of a single-qubit matrix :math:`U` in terms of + elementary operations. - The set of angles are chosen so as to implement :math:`U` up to a global phase. + Diagonal operations will be converted to a single ``RZ`` gate, while non-diagonal + operations will be converted to a ``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. - tol (float): The tolerance at which an angle is considered to be close enough - to 0. Needed to deal with varying precision across interfaces. + wire (Union[Wires, Sequence[int] or int]): The wire on which to apply the operation. Returns: - (float, float, float): A set of angles (:math:`\phi`, :math:`\theta`, :math:`\omega`) - that implement U as a sequence :math:`U = RZ(\omega) RY(\theta) RZ(\phi)`. + 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) @@ -91,26 +109,3 @@ def _zyz_decomposition(U, wire): 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)] - - -@qfunc_transform -def decompose_single_qubit_unitaries(tape): - """Quantum function transform to decomposes all instances of single-qubit QubitUnitary - operations to a sequence of rotations of the form ``RZ``, ``RY``, ``RZ``. - - Args: - tape (qml.tape.QuantumTape): A quantum tape. - """ - for op in tape.operations + tape.measurements: - if isinstance(op, qml.QubitUnitary): - dim_U = math.shape(op.parameters[0])[0] - - if dim_U != 2: - continue - - decomp = _zyz_decomposition(op.parameters[0], op.wires[0]) - - for d_op in decomp: - d_op.queue() - else: - op.queue() diff --git a/tests/transforms/test_decompose_single_qubit_unitaries.py b/tests/transforms/test_decompose_single_qubit_unitaries.py new file mode 100644 index 00000000000..27fc3d8fed4 --- /dev/null +++ b/tests/transforms/test_decompose_single_qubit_unitaries.py @@ -0,0 +1,318 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for the QubitUnitary decomposition transforms. +""" + +import pytest + +import pennylane as qml +from pennylane import numpy as np + +from pennylane.wires import Wires +from pennylane.transforms import decompose_single_qubit_unitaries + +from gate_data import I, Z, S, T, H, X + +single_qubit_decomps = [ + # First set of gates are diagonal and converted to RZ + (I, qml.RZ, [0.0]), + (Z, qml.RZ, [np.pi]), + (S, qml.RZ, [np.pi / 2]), + (T, qml.RZ, [np.pi / 4]), + (qml.RZ(0.3, wires=0).matrix, qml.RZ, [0.3]), + (qml.RZ(-0.5, wires=0).matrix, qml.RZ, [-0.5]), + # Next set of gates are non-diagonal and decomposed as Rots + (H, qml.Rot, [np.pi, np.pi / 2, 0.0]), + (X, qml.Rot, [0.0, np.pi, np.pi]), + (qml.Rot(0.2, 0.5, -0.3, wires=0).matrix, qml.Rot, [0.2, 0.5, -0.3]), + (np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix, qml.Rot, [-1.0, 2.0, -3.0]), +] + +# A simple quantum function for testing +def qfunc(U): + qml.Hadamard(wires="a") + qml.QubitUnitary(U, wires="a") + qml.CNOT(wires=["b", "a"]) + + +class TestDecomposeSingleQubitUnitaryTransform: + """Tests to ensure the transform itself works in all interfaces.""" + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries(self, U, expected_gate, expected_params): + """Test that the transform works in the autograd interface.""" + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose(ops[1].parameters, expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_torch(self, U, expected_gate, expected_params): + """Test that the transform works in the torch interface.""" + torch = pytest.importorskip("torch") + + U = torch.tensor(U, dtype=torch.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([x.detach() for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_tf(self, U, expected_gate, expected_params): + """Test that the transform works in the Tensorflow interface.""" + tf = pytest.importorskip("tensorflow") + + U = tf.Variable(U, dtype=tf.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([x.numpy() for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) + def test_decompose_single_qubit_unitaries_jax(self, U, expected_gate, expected_params): + """Test that the transform works in the JAX interface.""" + jax = pytest.importorskip("jax") + + U = jax.numpy.array(U, dtype=jax.numpy.complex64) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + + ops = qml.transforms.make_tape(transformed_qfunc)(U).operations + + assert len(ops) == 3 + + assert isinstance(ops[0], qml.Hadamard) + assert ops[0].wires == Wires("a") + + assert isinstance(ops[1], expected_gate) + assert ops[1].wires == Wires("a") + assert qml.math.allclose([jax.numpy.asarray(x) for x in ops[1].parameters], expected_params) + + assert isinstance(ops[2], qml.CNOT) + assert ops[2].wires == Wires(["b", "a"]) + + +# A simple circuit; we will test QubitUnitary on matrices constructed using trainable +# parameters, and RZ/RX are easy to write the matrices for. +def original_qfunc_for_grad(angles): + qml.Hadamard(wires="a") + qml.RZ(angles[0], wires="a") + qml.RX(angles[1], wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + +dev = qml.device("default.qubit", wires=["a", "b"]) + +angle_pairs = [(0.3, 0.3), (np.pi, -0.65), (0.0, np.pi / 2), (np.pi / 3, 0.0)] + + +class TestQubitUnitaryDifferentiability: + """Tests to ensure the transform is fully differentiable in all interfaces.""" + + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_gradient_qubit_unitary(self, z_rot, x_rot): + """Tests differentiability in autograd interface.""" + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + + Z_mat = np.array([[np.exp(-1j * z / 2), 0.0], [0.0, np.exp(1j * z / 2)]]) + + c = np.cos(x / 2) + s = np.sin(x / 2) * 1j + X_mat = np.array([[c, -s], [-s, c]]) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev) + + input = np.array([z_rot, x_rot], requires_grad=True) + assert qml.math.allclose(original_qnode(input), transformed_qnode(input)) + + original_grad = qml.grad(original_qnode)(input) + transformed_grad = qml.grad(transformed_qnode)(input) + + assert qml.math.allclose(original_grad, transformed_grad) + + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_gradient_qubit_unitary_torch(self, z_rot, x_rot): + """Tests differentiability in torch interface.""" + torch = pytest.importorskip("torch") + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + + # Had to do this in order to make a torch tensor of torch tensors + Z_mat = torch.stack( + [ + torch.exp(-1j * z / 2), + torch.tensor(0.0), + torch.tensor(0.0), + torch.exp(1j * z / 2), + ] + ).reshape(2, 2) + + # Variables need to be complex + c = torch.cos(x / 2).type(torch.complex64) + s = torch.sin(x / 2) * 1j + + X_mat = torch.stack([c, -s, -s, c]).reshape(2, 2) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") + original_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) + original_result = original_qnode(original_input) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") + transformed_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) + transformed_result = transformed_qnode(transformed_input) + + assert qml.math.allclose(original_result, transformed_result) + + original_result.backward() + transformed_result.backward() + + assert qml.math.allclose(original_input.grad, transformed_input.grad) + + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_tf(self, z_rot, x_rot): + """Tests differentiability in tensorflow interface.""" + tf = pytest.importorskip("tensorflow") + + def qfunc_with_qubit_unitary(angles): + z = tf.cast(angles[0], tf.complex64) + x = tf.cast(angles[1], tf.complex64) + + c = tf.cos(x / 2) + s = tf.sin(x / 2) * 1j + + Z_mat = tf.convert_to_tensor([[tf.exp(-1j * z / 2), 0.0], [0.0, tf.exp(1j * z / 2)]]) + X_mat = tf.convert_to_tensor([[c, -s], [-s, c]]) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") + original_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) + original_result = original_qnode(original_input) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") + transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) + transformed_result = transformed_qnode(transformed_input) + + assert qml.math.allclose(original_result, transformed_result) + + with tf.GradientTape() as tape: + loss = original_qnode(original_input) + original_grad = tape.gradient(loss, original_input) + + with tf.GradientTape() as tape: + loss = transformed_qnode(transformed_input) + transformed_grad = tape.gradient(loss, transformed_input) + + # For 64bit values, need to slightly increase the tolerance threshold + assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) + + @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) + def test_decompose_single_qubit_unitaries_jax(self, z_rot, x_rot): + """Tests differentiability in jax interface.""" + jax = pytest.importorskip("jax") + from jax import numpy as jnp + + def qfunc_with_qubit_unitary(angles): + z = angles[0] + x = angles[1] + + Z_mat = jnp.array([[jnp.exp(-1j * z / 2), 0.0], [0.0, jnp.exp(1j * z / 2)]]) + + c = jnp.cos(x / 2) + s = jnp.sin(x / 2) * 1j + X_mat = jnp.array([[c, -s], [-s, c]]) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + + # Setting the dtype to complex64 causes the gradients to be complex... + input = jnp.array([z_rot, x_rot], dtype=jnp.float64) + + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") + original_result = original_qnode(input) + + transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") + transformed_result = transformed_qnode(input) + assert qml.math.allclose(original_result, transformed_result) + + original_grad = jax.grad(original_qnode)(input) + transformed_grad = jax.grad(transformed_qnode)(input) + assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) diff --git a/tests/transforms/test_decompositions.py b/tests/transforms/test_decompositions.py index 55e089f6de2..17d0ce1880b 100644 --- a/tests/transforms/test_decompositions.py +++ b/tests/transforms/test_decompositions.py @@ -21,8 +21,7 @@ from pennylane import numpy as np from pennylane.wires import Wires -from pennylane.transforms import decompose_single_qubit_unitaries -from pennylane.transforms.decompositions import _zyz_decomposition +from pennylane.transforms.decompositions import zyz_decomposition from gate_data import I, Z, S, T, H, X @@ -41,24 +40,19 @@ (np.exp(1j * 0.02) * qml.Rot(-1.0, 2.0, -3.0, wires=0).matrix, qml.Rot, [-1.0, 2.0, -3.0]), ] -# A simple quantum function for testing -def qfunc(U): - qml.Hadamard(wires="a") - qml.QubitUnitary(U, wires="a") - qml.CNOT(wires=["b", "a"]) - class TestQubitUnitaryZYZDecomposition: - """Test that the decompsoitions are correct.""" + """Test that the decompositions are correct.""" def test_zyz_decomposition_invalid_input(self): + """Test that non-unitary operations throw errors when we try to decompose.""" with pytest.raises(ValueError, match="Operator must be unitary"): - _zyz_decomposition(I + H, Wires("a")) + zyz_decomposition(I + H, Wires("a")) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) def test_zyz_decomposition(self, U, expected_gate, expected_params): """Test that a one-qubit matrix in isolation is correctly decomposed.""" - obtained_gates = _zyz_decomposition(U, Wires("a")) + obtained_gates = zyz_decomposition(U, Wires("a")) assert len(obtained_gates) == 1 @@ -73,7 +67,7 @@ def test_zyz_decomposition_torch(self, U, expected_gate, expected_params): U = torch.tensor(U, dtype=torch.complex64) - obtained_gates = _zyz_decomposition(U, wire="a") + obtained_gates = zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 assert isinstance(obtained_gates[0], expected_gate) @@ -89,7 +83,7 @@ def test_zyz_decomposition_tf(self, U, expected_gate, expected_params): U = tf.Variable(U, dtype=tf.complex64) - obtained_gates = _zyz_decomposition(U, wire="a") + obtained_gates = zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 assert isinstance(obtained_gates[0], expected_gate) @@ -103,7 +97,7 @@ def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): U = jax.numpy.array(U, dtype=jax.numpy.complex64) - obtained_gates = _zyz_decomposition(U, wire="a") + obtained_gates = zyz_decomposition(U, wire="a") assert len(obtained_gates) == 1 assert isinstance(obtained_gates[0], expected_gate) @@ -111,270 +105,3 @@ def test_zyz_decomposition_jax(self, U, expected_gate, expected_params): assert qml.math.allclose( [jax.numpy.asarray(x) for x in obtained_gates[0].parameters], expected_params ) - - -class TestDecomposeSingleQubitUnitaryTransform: - """Tests to ensure the transform itself works in all interfaces.""" - - @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries(self, U, expected_gate, expected_params): - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) - - ops = qml.transforms.make_tape(transformed_qfunc)(U).operations - - assert len(ops) == 3 - - assert isinstance(ops[0], qml.Hadamard) - assert ops[0].wires == Wires("a") - - assert isinstance(ops[1], expected_gate) - assert ops[1].wires == Wires("a") - assert qml.math.allclose(ops[1].parameters, expected_params) - - assert isinstance(ops[2], qml.CNOT) - assert ops[2].wires == Wires(["b", "a"]) - - @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_torch(self, U, expected_gate, expected_params): - torch = pytest.importorskip("torch") - - U = torch.tensor(U, dtype=torch.complex64) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) - - ops = qml.transforms.make_tape(transformed_qfunc)(U).operations - - assert len(ops) == 3 - - assert isinstance(ops[0], qml.Hadamard) - assert ops[0].wires == Wires("a") - - assert isinstance(ops[1], expected_gate) - assert ops[1].wires == Wires("a") - assert qml.math.allclose([x.detach() for x in ops[1].parameters], expected_params) - - assert isinstance(ops[2], qml.CNOT) - assert ops[2].wires == Wires(["b", "a"]) - - @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_tf(self, U, expected_gate, expected_params): - tf = pytest.importorskip("tensorflow") - - U = tf.Variable(U, dtype=tf.complex64) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) - - ops = qml.transforms.make_tape(transformed_qfunc)(U).operations - - assert len(ops) == 3 - - assert isinstance(ops[0], qml.Hadamard) - assert ops[0].wires == Wires("a") - - assert isinstance(ops[1], expected_gate) - assert ops[1].wires == Wires("a") - assert qml.math.allclose([x.numpy() for x in ops[1].parameters], expected_params) - - assert isinstance(ops[2], qml.CNOT) - assert ops[2].wires == Wires(["b", "a"]) - - @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_jax(self, U, expected_gate, expected_params): - jax = pytest.importorskip("jax") - - U = jax.numpy.array(U, dtype=jax.numpy.complex64) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) - - ops = qml.transforms.make_tape(transformed_qfunc)(U).operations - - assert len(ops) == 3 - - assert isinstance(ops[0], qml.Hadamard) - assert ops[0].wires == Wires("a") - - assert isinstance(ops[1], expected_gate) - assert ops[1].wires == Wires("a") - assert qml.math.allclose([jax.numpy.asarray(x) for x in ops[1].parameters], expected_params) - - assert isinstance(ops[2], qml.CNOT) - assert ops[2].wires == Wires(["b", "a"]) - - -# A simple circuit; we will test QubitUnitary on matrices constructed using trainable -# parameters, and RZ/RX are easy to write the matrices for. -def original_qfunc_for_grad(angles): - qml.Hadamard(wires="a") - qml.RZ(angles[0], wires="a") - qml.RX(angles[1], wires="b") - qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliX(wires="a")) - - -dev = qml.device("default.qubit", wires=["a", "b"]) - -angle_pairs = [(0.3, 0.3), (np.pi, -0.65), (0.0, np.pi / 2), (np.pi / 3, 0.0)] - - -class TestQubitUnitaryDifferentiability: - """Tests to ensure the transform is fully differentiable in all interfaces.""" - - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_qubit_unitary(self, z_rot, x_rot): - """Tests differentiability in autograd interface.""" - - def qfunc_with_qubit_unitary(angles): - z = angles[0] - x = angles[1] - - Z_mat = np.array([[np.exp(-1j * z / 2), 0.0], [0.0, np.exp(1j * z / 2)]]) - - c = np.cos(x / 2) - s = np.sin(x / 2) * 1j - X_mat = np.array([[c, -s], [-s, c]]) - - qml.Hadamard(wires="a") - qml.QubitUnitary(Z_mat, wires="a") - qml.QubitUnitary(X_mat, wires="b") - qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliX(wires="a")) - - original_qnode = qml.QNode(original_qfunc_for_grad, dev) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev) - - input = np.array([z_rot, x_rot], requires_grad=True) - assert qml.math.allclose(original_qnode(input), transformed_qnode(input)) - - original_grad = qml.grad(original_qnode)(input) - transformed_grad = qml.grad(transformed_qnode)(input) - - assert qml.math.allclose(original_grad, transformed_grad) - - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_qubit_unitary_torch(self, z_rot, x_rot): - """Tests differentiability in torch interface.""" - torch = pytest.importorskip("torch") - - def qfunc_with_qubit_unitary(angles): - z = angles[0] - x = angles[1] - - # Had to do this in order to make a torch tensor of torch tensors - Z_mat = torch.stack( - [ - torch.exp(-1j * z / 2), - torch.tensor(0.0), - torch.tensor(0.0), - torch.exp(1j * z / 2), - ] - ).reshape(2, 2) - - # Variables need to be complex - c = torch.cos(x / 2).type(torch.complex64) - s = torch.sin(x / 2) * 1j - - X_mat = torch.stack([c, -s, -s, c]).reshape(2, 2) - - qml.Hadamard(wires="a") - qml.QubitUnitary(Z_mat, wires="a") - qml.QubitUnitary(X_mat, wires="b") - qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliX(wires="a")) - - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") - original_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) - original_result = original_qnode(original_input) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") - transformed_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) - transformed_result = transformed_qnode(transformed_input) - - assert qml.math.allclose(original_result, transformed_result) - - original_result.backward() - transformed_result.backward() - - assert qml.math.allclose(original_input.grad, transformed_input.grad) - - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_tf(self, z_rot, x_rot): - """Tests differentiability in tensorflow interface.""" - tf = pytest.importorskip("tensorflow") - - def qfunc_with_qubit_unitary(angles): - z = tf.cast(angles[0], tf.complex64) - x = tf.cast(angles[1], tf.complex64) - - c = tf.cos(x / 2) - s = tf.sin(x / 2) * 1j - - Z_mat = tf.convert_to_tensor([[tf.exp(-1j * z / 2), 0.0], [0.0, tf.exp(1j * z / 2)]]) - X_mat = tf.convert_to_tensor([[c, -s], [-s, c]]) - - qml.Hadamard(wires="a") - qml.QubitUnitary(Z_mat, wires="a") - qml.QubitUnitary(X_mat, wires="b") - qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliX(wires="a")) - - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") - original_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) - original_result = original_qnode(original_input) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") - transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) - transformed_result = transformed_qnode(transformed_input) - - assert qml.math.allclose(original_result, transformed_result) - - with tf.GradientTape() as tape: - loss = original_qnode(original_input) - original_grad = tape.gradient(loss, original_input) - - with tf.GradientTape() as tape: - loss = transformed_qnode(transformed_input) - transformed_grad = tape.gradient(loss, transformed_input) - - # For 64bit values, need to slightly increase the tolerance threshold - assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) - - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_jax(self, z_rot, x_rot): - """Tests differentiability in jax interface.""" - jax = pytest.importorskip("jax") - from jax import numpy as jnp - - def qfunc_with_qubit_unitary(angles): - z = angles[0] - x = angles[1] - - Z_mat = jnp.array([[jnp.exp(-1j * z / 2), 0.0], [0.0, jnp.exp(1j * z / 2)]]) - - c = jnp.cos(x / 2) - s = jnp.sin(x / 2) * 1j - X_mat = jnp.array([[c, -s], [-s, c]]) - - qml.Hadamard(wires="a") - qml.QubitUnitary(Z_mat, wires="a") - qml.QubitUnitary(X_mat, wires="b") - qml.CNOT(wires=["b", "a"]) - return qml.expval(qml.PauliX(wires="a")) - - # Setting the dtype to complex64 causes the gradients to be complex... - input = jnp.array([z_rot, x_rot], dtype=jnp.float64) - - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") - original_result = original_qnode(input) - - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") - transformed_result = transformed_qnode(input) - assert qml.math.allclose(original_result, transformed_result) - - original_grad = jax.grad(original_qnode)(input) - transformed_grad = jax.grad(transformed_qnode)(input) - assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) From ef093560fdf87d7b18fe449076ab4720315196fd Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 09:50:28 -0400 Subject: [PATCH 22/37] Remove redundant checks. --- pennylane/ops/qubit.py | 2 +- pennylane/transforms/decompose_single_qubit_unitaries.py | 5 ++--- pennylane/transforms/decompositions/single_qubit_unitary.py | 6 ------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 90b598e32ac..67a2553256e 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2160,7 +2160,7 @@ def _matrix(cls, *params): @staticmethod def decomposition(U, wires): # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ - if qml.math.shape(U)[0] == 2: + if qml.math.shape(U) == (2, 2): wire = Wires(wires)[0] decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire) return decomp_ops diff --git a/pennylane/transforms/decompose_single_qubit_unitaries.py b/pennylane/transforms/decompose_single_qubit_unitaries.py index e641fd6bfdf..7b12e65955c 100644 --- a/pennylane/transforms/decompose_single_qubit_unitaries.py +++ b/pennylane/transforms/decompose_single_qubit_unitaries.py @@ -74,9 +74,8 @@ def qfunc(): """ for op in tape.operations + tape.measurements: if isinstance(op, qml.QubitUnitary): - dim_U = math.shape(op.parameters[0])[0] - - if dim_U != 2: + # Only act on single-qubit unitary operations + if math.shape(op.parameters[0]) != (2, 2): continue decomp = zyz_decomposition(op.parameters[0], op.wires[0]) diff --git a/pennylane/transforms/decompositions/single_qubit_unitary.py b/pennylane/transforms/decompositions/single_qubit_unitary.py index 7eb14e4c0df..6344e9b28fb 100644 --- a/pennylane/transforms/decompositions/single_qubit_unitary.py +++ b/pennylane/transforms/decompositions/single_qubit_unitary.py @@ -28,12 +28,6 @@ def _convert_to_su2(U): array[complex]: A :math:`2 \times 2` matrix in :math:`SU(2)` that is equivalent to U up to a global phase. """ - shape = math.shape(U) - - # Check dimensions - if shape != (2, 2): - raise ValueError(f"Cannot convert matrix with shape {shape} to SU(2).") - # 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.") From b9480be75730b670910fd093c79304d38f18d591 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:00:03 -0400 Subject: [PATCH 23/37] Apply suggestions from code review Co-authored-by: Josh Izaac --- .../transforms/decompose_single_qubit_unitaries.py | 10 +++++----- .../transforms/decompositions/single_qubit_unitary.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pennylane/transforms/decompose_single_qubit_unitaries.py b/pennylane/transforms/decompose_single_qubit_unitaries.py index 7b12e65955c..5b9f48e9f96 100644 --- a/pennylane/transforms/decompose_single_qubit_unitaries.py +++ b/pennylane/transforms/decompose_single_qubit_unitaries.py @@ -22,15 +22,15 @@ @qfunc_transform def decompose_single_qubit_unitaries(tape): - """Quantum function transform to decomposes all instances of single-qubit QubitUnitary + """Quantum function transform to decomposes all instances of single-qubit :class:`~.QubitUnitary` operations to parametrized single-qubit operations. - Diagonal operations will be converted to a single ``RZ`` gate, while non-diagonal - operations will be converted to a ``Rot`` gate that implements the original operation + 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. Args: - tape (qml.tape.QuantumTape): A quantum tape. + qfunc (function): a quantum function **Example** @@ -52,7 +52,7 @@ def decompose_single_qubit_unitaries(tape): def qfunc(): qml.QubitUnitary(U, wires=0) - return qml.expval(qml.PauliZ(0) + return qml.expval(qml.PauliZ(0)) The original circuit is: diff --git a/pennylane/transforms/decompositions/single_qubit_unitary.py b/pennylane/transforms/decompositions/single_qubit_unitary.py index 6344e9b28fb..1ce3221196c 100644 --- a/pennylane/transforms/decompositions/single_qubit_unitary.py +++ b/pennylane/transforms/decompositions/single_qubit_unitary.py @@ -47,8 +47,8 @@ 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 ``RZ`` gate, while non-diagonal - operations will be converted to a ``Rot`` gate that implements the original operation + 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: From b6215467406110e2d3b9fd84f6b6b9a3eba8c16e Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 11:44:22 -0400 Subject: [PATCH 24/37] Rename transform. --- pennylane/transforms/__init__.py | 4 ++-- ...ecompose_single_qubit_unitaries.py => unitary_to_rot.py} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename pennylane/transforms/{decompose_single_qubit_unitaries.py => unitary_to_rot.py} (93%) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index 232baea8417..d1fb3aa1140 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -45,7 +45,7 @@ ~adjoint ~ctrl - ~transforms.decompose_single_qubit_unitaries + ~transforms.unitary_to_rot ~transforms.invisible ~apply_controlled_Q ~quantum_monte_carlo @@ -82,7 +82,6 @@ from .classical_jacobian import classical_jacobian from .control import ControlledOperation, ctrl from .decompositions import zyz_decomposition -from .decompose_single_qubit_unitaries import decompose_single_qubit_unitaries from .draw import draw from .hamiltonian_expand import hamiltonian_expand from .invisible import invisible @@ -90,3 +89,4 @@ from .metric_tensor import metric_tensor, metric_tensor_tape from .specs import specs from .qmc import apply_controlled_Q, quantum_monte_carlo +from .unitary_to_rot import unitary_to_rot diff --git a/pennylane/transforms/decompose_single_qubit_unitaries.py b/pennylane/transforms/unitary_to_rot.py similarity index 93% rename from pennylane/transforms/decompose_single_qubit_unitaries.py rename to pennylane/transforms/unitary_to_rot.py index 5b9f48e9f96..190c575371e 100644 --- a/pennylane/transforms/decompose_single_qubit_unitaries.py +++ b/pennylane/transforms/unitary_to_rot.py @@ -21,7 +21,7 @@ @qfunc_transform -def decompose_single_qubit_unitaries(tape): +def unitary_to_rot(tape): """Quantum function transform to decomposes all instances of single-qubit :class:`~.QubitUnitary` operations to parametrized single-qubit operations. @@ -43,7 +43,7 @@ def decompose_single_qubit_unitaries(tape): [ 0.25053735+0.75164238j, 0.60700543-0.06171855j] ]) - The ``decompose_single_qubit_unitaries`` transform enables us to decompose + The ``unitary_to_rot`` transform enables us to decompose such numerical operations (as well as unitaries that may be defined by parameters within the QNode, and instantiated therein), while preserving differentiability. @@ -66,7 +66,7 @@ def qfunc(): We can use the transform to decompose the gate: - >>> transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + >>> transformed_qfunc = unitary_to_rot(qfunc) >>> transformed_qnode = qml.QNode(transformed_qfunc, dev) >>> print(qml.draw(transformed_qnode)()) 0: ──Rot(-1.35, 1.83, -0.606)──┤ ⟨Z⟩ From a40f05b216b663f334701640b95b1219421dcde9 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 11:45:34 -0400 Subject: [PATCH 25/37] Update tests to new name. --- ...it_unitaries.py => test_unitary_to_rot.py} | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) rename tests/transforms/{test_decompose_single_qubit_unitaries.py => test_unitary_to_rot.py} (90%) diff --git a/tests/transforms/test_decompose_single_qubit_unitaries.py b/tests/transforms/test_unitary_to_rot.py similarity index 90% rename from tests/transforms/test_decompose_single_qubit_unitaries.py rename to tests/transforms/test_unitary_to_rot.py index 27fc3d8fed4..56c5d761a15 100644 --- a/tests/transforms/test_decompose_single_qubit_unitaries.py +++ b/tests/transforms/test_unitary_to_rot.py @@ -21,7 +21,7 @@ from pennylane import numpy as np from pennylane.wires import Wires -from pennylane.transforms import decompose_single_qubit_unitaries +from pennylane.transforms import unitary_to_rot from gate_data import I, Z, S, T, H, X @@ -51,9 +51,9 @@ class TestDecomposeSingleQubitUnitaryTransform: """Tests to ensure the transform itself works in all interfaces.""" @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries(self, U, expected_gate, expected_params): + def test_unitary_to_rot(self, U, expected_gate, expected_params): """Test that the transform works in the autograd interface.""" - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + transformed_qfunc = unitary_to_rot(qfunc) ops = qml.transforms.make_tape(transformed_qfunc)(U).operations @@ -70,13 +70,13 @@ def test_decompose_single_qubit_unitaries(self, U, expected_gate, expected_param assert ops[2].wires == Wires(["b", "a"]) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_torch(self, U, expected_gate, expected_params): + def test_unitary_to_rot_torch(self, U, expected_gate, expected_params): """Test that the transform works in the torch interface.""" torch = pytest.importorskip("torch") U = torch.tensor(U, dtype=torch.complex64) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + transformed_qfunc = unitary_to_rot(qfunc) ops = qml.transforms.make_tape(transformed_qfunc)(U).operations @@ -93,13 +93,13 @@ def test_decompose_single_qubit_unitaries_torch(self, U, expected_gate, expected assert ops[2].wires == Wires(["b", "a"]) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_tf(self, U, expected_gate, expected_params): + def test_unitary_to_rot_tf(self, U, expected_gate, expected_params): """Test that the transform works in the Tensorflow interface.""" tf = pytest.importorskip("tensorflow") U = tf.Variable(U, dtype=tf.complex64) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + transformed_qfunc = unitary_to_rot(qfunc) ops = qml.transforms.make_tape(transformed_qfunc)(U).operations @@ -116,13 +116,13 @@ def test_decompose_single_qubit_unitaries_tf(self, U, expected_gate, expected_pa assert ops[2].wires == Wires(["b", "a"]) @pytest.mark.parametrize("U,expected_gate,expected_params", single_qubit_decomps) - def test_decompose_single_qubit_unitaries_jax(self, U, expected_gate, expected_params): + def test_unitary_to_rot_jax(self, U, expected_gate, expected_params): """Test that the transform works in the JAX interface.""" jax = pytest.importorskip("jax") U = jax.numpy.array(U, dtype=jax.numpy.complex64) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc) + transformed_qfunc = unitary_to_rot(qfunc) ops = qml.transforms.make_tape(transformed_qfunc)(U).operations @@ -179,7 +179,7 @@ def qfunc_with_qubit_unitary(angles): original_qnode = qml.QNode(original_qfunc_for_grad, dev) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev) input = np.array([z_rot, x_rot], requires_grad=True) @@ -225,7 +225,7 @@ def qfunc_with_qubit_unitary(angles): original_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) original_result = original_qnode(original_input) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") transformed_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) transformed_result = transformed_qnode(transformed_input) @@ -238,7 +238,7 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_input.grad, transformed_input.grad) @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_tf(self, z_rot, x_rot): + def test_unitary_to_rot_tf(self, z_rot, x_rot): """Tests differentiability in tensorflow interface.""" tf = pytest.importorskip("tensorflow") @@ -262,7 +262,7 @@ def qfunc_with_qubit_unitary(angles): original_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) original_result = original_qnode(original_input) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) transformed_result = transformed_qnode(transformed_input) @@ -281,7 +281,7 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_decompose_single_qubit_unitaries_jax(self, z_rot, x_rot): + def test_unitary_to_rot_jax(self, z_rot, x_rot): """Tests differentiability in jax interface.""" jax = pytest.importorskip("jax") from jax import numpy as jnp @@ -308,7 +308,7 @@ def qfunc_with_qubit_unitary(angles): original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") original_result = original_qnode(input) - transformed_qfunc = decompose_single_qubit_unitaries(qfunc_with_qubit_unitary) + transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") transformed_result = transformed_qnode(input) assert qml.math.allclose(original_result, transformed_result) From 97ee4db9698d390d01753c6c69f2d7c30a3a6dac Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 12:09:18 -0400 Subject: [PATCH 26/37] Raise NotImplementedError and add new test. Fixes qmc test. --- pennylane/ops/qubit.py | 2 +- tests/ops/test_qubit_ops.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index c630613072b..fb9fad7cf9b 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2166,7 +2166,7 @@ def decomposition(U, wires): decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire) return decomp_ops - return NotImplementedError("Decompositions only supported for single-qubit unitaries") + raise NotImplementedError("Decompositions only supported for single-qubit unitaries") def adjoint(self): return QubitUnitary(qml.math.T(qml.math.conj(self.matrix)), wires=self.wires) diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py index 5f6b2b89fe6..da5f6b544a4 100644 --- a/tests/ops/test_qubit_ops.py +++ b/tests/ops/test_qubit_ops.py @@ -1462,12 +1462,20 @@ def test_qubit_unitary_not_matrix_exception(self, U): ], ) def test_qubit_unitary_decomposition(self, U, expected_gate, expected_params): + """Tests that single-qubit QubitUnitary decompositions are performed.""" decomp = qml.QubitUnitary.decomposition(U, wires=0) assert len(decomp) == 1 assert isinstance(decomp[0], expected_gate) assert np.allclose(decomp[0].parameters, expected_params) + def test_qubit_unitary_decomposition_multiqubit_invalid(self): + """Test that QubitUnitary is not decomposed for more than a single qubit.""" + U = qml.CRZ(0.3, wires=[0, 1]).matrix + + with pytest.raises(NotImplementedError, match="only supported for single-qubit"): + qml.QubitUnitary.decomposition(U, wires=[0, 1]) + def test_iswap_eigenval(self): """Tests that the ISWAP eigenvalue matches the numpy eigenvalues of the ISWAP matrix""" op = qml.ISWAP(wires=[0, 1]) From 723229493a5b9c640223d3a5e8e0d122e5318c3f Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 12:22:03 -0400 Subject: [PATCH 27/37] Update tests to use 2-qubit unitary. --- tests/circuit_graph/test_qasm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index 0c5962381f5..bb06d88108d 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -227,7 +227,7 @@ def test_basis_state_initialization_decomposition(self): def test_unsupported_gate(self): """Test an exception is raised if an unsupported operation is applied.""" - U = np.array([[1, 1], [1, -1]]) / np.sqrt(2) + U = qml.CRY(0.5, wires=[0, 1]).matrix with qml.tape.QuantumTape() as circuit: qml.S(wires=0), qml.QubitUnitary(U, wires=[0, 1]) @@ -578,13 +578,13 @@ def qnode(state=None): def test_unsupported_gate(self): """Test an exception is raised if an unsupported operation is applied.""" - U = np.array([[1, 1], [1, -1]]) / np.sqrt(2) - dev = qml.device("default.qubit", wires=1) + U = qml.CRY(0.5, wires=[0, 1]).matrix + dev = qml.device("default.qubit", wires=2) @qml.qnode(dev) def qnode(): qml.S(wires=0) - qml.QubitUnitary(U, wires=0) + qml.QubitUnitary(U, wires=[0, 1]) return qml.expval(qml.PauliZ(0)) qnode() From 262fbafaa2e46b1ba2a5fd8abc95c5acf3c7ef19 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 12:25:21 -0400 Subject: [PATCH 28/37] Change unsupported QASM gate to DoubleExcitationPlus. --- tests/circuit_graph/test_qasm.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/circuit_graph/test_qasm.py b/tests/circuit_graph/test_qasm.py index bb06d88108d..03f9a34d550 100644 --- a/tests/circuit_graph/test_qasm.py +++ b/tests/circuit_graph/test_qasm.py @@ -227,12 +227,12 @@ def test_basis_state_initialization_decomposition(self): def test_unsupported_gate(self): """Test an exception is raised if an unsupported operation is applied.""" - U = qml.CRY(0.5, wires=[0, 1]).matrix - with qml.tape.QuantumTape() as circuit: - qml.S(wires=0), qml.QubitUnitary(U, wires=[0, 1]) + qml.S(wires=0), qml.DoubleExcitationPlus(0.5, wires=[0, 1, 2, 3]) - with pytest.raises(ValueError, match="QubitUnitary not supported by the QASM serializer"): + with pytest.raises( + ValueError, match="DoubleExcitationPlus not supported by the QASM serializer" + ): res = circuit.to_openqasm() def test_rotations(self): @@ -578,18 +578,19 @@ def qnode(state=None): def test_unsupported_gate(self): """Test an exception is raised if an unsupported operation is applied.""" - U = qml.CRY(0.5, wires=[0, 1]).matrix - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=4) @qml.qnode(dev) def qnode(): qml.S(wires=0) - qml.QubitUnitary(U, wires=[0, 1]) + qml.DoubleExcitationPlus(0.5, wires=[0, 1, 2, 3]) return qml.expval(qml.PauliZ(0)) qnode() - with pytest.raises(ValueError, match="QubitUnitary not supported by the QASM serializer"): + with pytest.raises( + ValueError, match="DoubleExcitationPlus not supported by the QASM serializer" + ): qnode.qtape.to_openqasm() def test_rotations(self): From 1489d81f14a2e86107d781204982375fff286f64 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 29 Jun 2021 12:50:46 -0400 Subject: [PATCH 29/37] Fix gradient test names and add min version for torch. --- tests/transforms/test_unitary_to_rot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/transforms/test_unitary_to_rot.py b/tests/transforms/test_unitary_to_rot.py index 56c5d761a15..c087b1d9c29 100644 --- a/tests/transforms/test_unitary_to_rot.py +++ b/tests/transforms/test_unitary_to_rot.py @@ -158,7 +158,7 @@ class TestQubitUnitaryDifferentiability: """Tests to ensure the transform is fully differentiable in all interfaces.""" @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_qubit_unitary(self, z_rot, x_rot): + def test_gradient_unitary_to_rot(self, z_rot, x_rot): """Tests differentiability in autograd interface.""" def qfunc_with_qubit_unitary(angles): @@ -191,9 +191,9 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_grad, transformed_grad) @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_qubit_unitary_torch(self, z_rot, x_rot): + def test_gradient_unitary_to_rot_torch(self, z_rot, x_rot): """Tests differentiability in torch interface.""" - torch = pytest.importorskip("torch") + torch = pytest.importorskip("torch", minversion="1.8") def qfunc_with_qubit_unitary(angles): z = angles[0] @@ -238,7 +238,7 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_input.grad, transformed_input.grad) @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_unitary_to_rot_tf(self, z_rot, x_rot): + def test_gradient_unitary_to_rot_tf(self, z_rot, x_rot): """Tests differentiability in tensorflow interface.""" tf = pytest.importorskip("tensorflow") @@ -281,7 +281,7 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_unitary_to_rot_jax(self, z_rot, x_rot): + def test_gradient_unitary_to_rot_jax(self, z_rot, x_rot): """Tests differentiability in jax interface.""" jax = pytest.importorskip("jax") from jax import numpy as jnp From 379ea3ffcd710ba820e46ff4c8452986a2b7faab Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Mon, 5 Jul 2021 10:32:23 -0400 Subject: [PATCH 30/37] Add backprop diff method to tests. --- tests/transforms/test_unitary_to_rot.py | 61 +++++++++++++++---------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/transforms/test_unitary_to_rot.py b/tests/transforms/test_unitary_to_rot.py index c087b1d9c29..2f26b11d7e3 100644 --- a/tests/transforms/test_unitary_to_rot.py +++ b/tests/transforms/test_unitary_to_rot.py @@ -17,6 +17,8 @@ import pytest +from itertools import product + import pennylane as qml from pennylane import numpy as np @@ -151,14 +153,16 @@ def original_qfunc_for_grad(angles): dev = qml.device("default.qubit", wires=["a", "b"]) -angle_pairs = [(0.3, 0.3), (np.pi, -0.65), (0.0, np.pi / 2), (np.pi / 3, 0.0)] +angle_pairs = [[0.3, 0.3], [np.pi, -0.65], [0.0, np.pi / 2], [np.pi / 3, 0.0]] +diff_methods = ["parameter-shift", "backprop"] +angle_diff_pairs = list(product(angle_pairs, diff_methods)) class TestQubitUnitaryDifferentiability: """Tests to ensure the transform is fully differentiable in all interfaces.""" - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_unitary_to_rot(self, z_rot, x_rot): + @pytest.mark.parametrize("rot_angles,diff_method", angle_diff_pairs) + def test_gradient_unitary_to_rot(self, rot_angles, diff_method): """Tests differentiability in autograd interface.""" def qfunc_with_qubit_unitary(angles): @@ -177,12 +181,12 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - original_qnode = qml.QNode(original_qfunc_for_grad, dev) + original_qnode = qml.QNode(original_qfunc_for_grad, dev, diff_method=diff_method) transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev) + transformed_qnode = qml.QNode(transformed_qfunc, dev, diff_method=diff_method) - input = np.array([z_rot, x_rot], requires_grad=True) + input = np.array(rot_angles, requires_grad=True) assert qml.math.allclose(original_qnode(input), transformed_qnode(input)) original_grad = qml.grad(original_qnode)(input) @@ -190,9 +194,10 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_grad, transformed_grad) - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_unitary_to_rot_torch(self, z_rot, x_rot): - """Tests differentiability in torch interface.""" + @pytest.mark.parametrize("rot_angles", angle_pairs) + def test_gradient_unitary_to_rot_torch(self, rot_angles): + """Tests differentiability in torch interface. Torch interface doesn't use + backprop so we test only with parameter-shift.""" torch = pytest.importorskip("torch", minversion="1.8") def qfunc_with_qubit_unitary(angles): @@ -221,13 +226,17 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="torch") - original_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) + original_qnode = qml.QNode( + original_qfunc_for_grad, dev, interface="torch", diff_method="parameter-shift" + ) + original_input = torch.tensor(rot_angles, dtype=torch.float64, requires_grad=True) original_result = original_qnode(original_input) transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="torch") - transformed_input = torch.tensor([z_rot, x_rot], dtype=torch.float64, requires_grad=True) + transformed_qnode = qml.QNode( + transformed_qfunc, dev, interface="torch", diff_method="parameter-shift" + ) + transformed_input = torch.tensor(rot_angles, dtype=torch.float64, requires_grad=True) transformed_result = transformed_qnode(transformed_input) assert qml.math.allclose(original_result, transformed_result) @@ -237,8 +246,8 @@ def qfunc_with_qubit_unitary(angles): assert qml.math.allclose(original_input.grad, transformed_input.grad) - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_unitary_to_rot_tf(self, z_rot, x_rot): + @pytest.mark.parametrize("rot_angles,diff_method", angle_diff_pairs) + def test_gradient_unitary_to_rot_tf(self, rot_angles, diff_method): """Tests differentiability in tensorflow interface.""" tf = pytest.importorskip("tensorflow") @@ -258,13 +267,17 @@ def qfunc_with_qubit_unitary(angles): qml.CNOT(wires=["b", "a"]) return qml.expval(qml.PauliX(wires="a")) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="tf") - original_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) + original_qnode = qml.QNode( + original_qfunc_for_grad, dev, interface="tf", diff_method=diff_method + ) + original_input = tf.Variable(rot_angles, dtype=tf.float64) original_result = original_qnode(original_input) transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="tf") - transformed_input = tf.Variable([z_rot, x_rot], dtype=tf.float64) + transformed_qnode = qml.QNode( + transformed_qfunc, dev, interface="tf", diff_method=diff_method + ) + transformed_input = tf.Variable(rot_angles, dtype=tf.float64) transformed_result = transformed_qnode(transformed_input) assert qml.math.allclose(original_result, transformed_result) @@ -280,8 +293,8 @@ def qfunc_with_qubit_unitary(angles): # For 64bit values, need to slightly increase the tolerance threshold assert qml.math.allclose(original_grad, transformed_grad, atol=1e-7) - @pytest.mark.parametrize("z_rot,x_rot", angle_pairs) - def test_gradient_unitary_to_rot_jax(self, z_rot, x_rot): + @pytest.mark.parametrize("rot_angles,diff_method", angle_diff_pairs) + def test_gradient_unitary_to_rot_jax(self, rot_angles, diff_method): """Tests differentiability in jax interface.""" jax = pytest.importorskip("jax") from jax import numpy as jnp @@ -303,13 +316,13 @@ def qfunc_with_qubit_unitary(angles): return qml.expval(qml.PauliX(wires="a")) # Setting the dtype to complex64 causes the gradients to be complex... - input = jnp.array([z_rot, x_rot], dtype=jnp.float64) + input = jnp.array(rot_angles, dtype=jnp.float64) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax") + original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax", diff_method=diff_method) original_result = original_qnode(input) transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax") + transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax", diff_method=diff_method) transformed_result = transformed_qnode(input) assert qml.math.allclose(original_result, transformed_result) From 0dd4ce095a9925d81b54f10e9ad81f15ba4e51ee Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Mon, 5 Jul 2021 16:40:09 -0400 Subject: [PATCH 31/37] Switch transform to using qml.apply. --- pennylane/transforms/unitary_to_rot.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pennylane/transforms/unitary_to_rot.py b/pennylane/transforms/unitary_to_rot.py index 190c575371e..5286af2bc26 100644 --- a/pennylane/transforms/unitary_to_rot.py +++ b/pennylane/transforms/unitary_to_rot.py @@ -15,7 +15,6 @@ A transform for decomposing arbitrary single-qubit QubitUnitary gates into elementary gates. """ import pennylane as qml -from pennylane import math from pennylane.transforms import qfunc_transform from pennylane.transforms.decompositions import zyz_decomposition @@ -75,12 +74,12 @@ def qfunc(): for op in tape.operations + tape.measurements: if isinstance(op, qml.QubitUnitary): # Only act on single-qubit unitary operations - if math.shape(op.parameters[0]) != (2, 2): + if qml.math.shape(op.parameters[0]) != (2, 2): continue decomp = zyz_decomposition(op.parameters[0], op.wires[0]) for d_op in decomp: - d_op.queue() + qml.apply(d_op) else: - op.queue() + qml.apply(op) From 8c41a0de8aea16bc17e2f1525f5d1bcd10fa4293 Mon Sep 17 00:00:00 2001 From: Josh Izaac Date: Tue, 6 Jul 2021 17:25:09 +0800 Subject: [PATCH 32/37] fix tests --- tests/transforms/test_unitary_to_rot.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/transforms/test_unitary_to_rot.py b/tests/transforms/test_unitary_to_rot.py index 2f26b11d7e3..72de3a5af82 100644 --- a/tests/transforms/test_unitary_to_rot.py +++ b/tests/transforms/test_unitary_to_rot.py @@ -252,8 +252,8 @@ def test_gradient_unitary_to_rot_tf(self, rot_angles, diff_method): tf = pytest.importorskip("tensorflow") def qfunc_with_qubit_unitary(angles): - z = tf.cast(angles[0], tf.complex64) - x = tf.cast(angles[1], tf.complex64) + z = tf.cast(angles[0], tf.complex128) + x = tf.cast(angles[1], tf.complex128) c = tf.cos(x / 2) s = tf.sin(x / 2) * 1j @@ -288,6 +288,7 @@ def qfunc_with_qubit_unitary(angles): with tf.GradientTape() as tape: loss = transformed_qnode(transformed_input) + transformed_grad = tape.gradient(loss, transformed_input) # For 64bit values, need to slightly increase the tolerance threshold @@ -318,11 +319,15 @@ def qfunc_with_qubit_unitary(angles): # Setting the dtype to complex64 causes the gradients to be complex... input = jnp.array(rot_angles, dtype=jnp.float64) - original_qnode = qml.QNode(original_qfunc_for_grad, dev, interface="jax", diff_method=diff_method) + original_qnode = qml.QNode( + original_qfunc_for_grad, dev, interface="jax", diff_method=diff_method + ) original_result = original_qnode(input) transformed_qfunc = unitary_to_rot(qfunc_with_qubit_unitary) - transformed_qnode = qml.QNode(transformed_qfunc, dev, interface="jax", diff_method=diff_method) + transformed_qnode = qml.QNode( + transformed_qfunc, dev, interface="jax", diff_method=diff_method + ) transformed_result = transformed_qnode(input) assert qml.math.allclose(original_result, transformed_result) From 40fdf67b52dc61fb02832a9838d28ae8c76aaa2b Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 6 Jul 2021 09:23:35 -0400 Subject: [PATCH 33/37] Fix to work with qml.apply; don't requeue ops. --- pennylane/transforms/unitary_to_rot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pennylane/transforms/unitary_to_rot.py b/pennylane/transforms/unitary_to_rot.py index 5286af2bc26..cd05fccbdee 100644 --- a/pennylane/transforms/unitary_to_rot.py +++ b/pennylane/transforms/unitary_to_rot.py @@ -77,9 +77,6 @@ def qfunc(): if qml.math.shape(op.parameters[0]) != (2, 2): continue - decomp = zyz_decomposition(op.parameters[0], op.wires[0]) - - for d_op in decomp: - qml.apply(d_op) + zyz_decomposition(op.parameters[0], op.wires[0]) else: qml.apply(op) From 8a20067cf0cfe07a9333bc4d72b7667e9a962beb Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 6 Jul 2021 13:58:48 -0400 Subject: [PATCH 34/37] Add zyz_decomp to docs. --- pennylane/transforms/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index d1fb3aa1140..9a18760fb8f 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -50,6 +50,14 @@ ~apply_controlled_Q ~quantum_monte_carlo +There are also utility functions and decompositions available that assist with +both transforms, and decompositions within the larger PennyLane codebase. + +.. autosummary:: + :toctree: api + + ~transforms.zyz_decomposition + Transforms that act on tapes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 26575eaf37a82fe0ff16e9bc808bba50b23a8e0c Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Tue, 6 Jul 2021 14:27:20 -0400 Subject: [PATCH 35/37] Clarify decomposition docstring. --- pennylane/ops/qubit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index 0e50ef56741..5002aa12f13 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -2161,7 +2161,8 @@ def _matrix(cls, *params): @staticmethod def decomposition(U, wires): - # Decompose arbitrary single-qubit unitaries as the form RZ RY RZ + # Decomposes arbitrary single-qubit unitaries as Rot gates (RZ - RY - RZ format), + # or a single RZ for diagonal matrices. if qml.math.shape(U) == (2, 2): wire = Wires(wires)[0] decomp_ops = qml.transforms.decompositions.zyz_decomposition(U, wire) From 03bd4dd26a08eea9a2e096c09d217e6285853380 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Wed, 7 Jul 2021 08:45:03 -0400 Subject: [PATCH 36/37] Update CHANGELOG. --- .github/CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b2545a8e10c..2d2a9b8b74f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,61 @@

New features since last release

+* A decomposition has been added to ``QubitUnitary`` that makes the + single-qubit case fully differentiable in all interfaces. Furthermore, + a quantum function transform, ``unitary_to_rot()``, has been added to decompose all + single-qubit instances of ``QubitUnitary`` in a quantum circuit. + [(#1427)](https://github.com/PennyLaneAI/pennylane/pull/1427). + + Instances of ``QubitUnitary`` may now be decomposed directly to ``Rot`` + operations, or ``RZ`` operations if the input matrix is diagonal. For + example, let + + ```python + >>> U = np.array([ + [-0.28829348-0.78829734j, 0.30364367+0.45085995j], + [ 0.53396245-0.10177564j, 0.76279558-0.35024096j] + ]) + ``` + + Then, we can compute the decomposition as: + + ```pycon + >>> qml.QubitUnitary.decomposition(U, wires=0) + [Rot(-0.24209530281458358, 1.1493817777199102, 1.733058145303424, wires=[0])] + ``` + + We can also apply the transform directly to a quantum function, and compute the + gradients of parameters used to construct the unitary matrices. + + ```python + def qfunc_with_qubit_unitary(angles): + z, x = angles[0], angles[1] + + Z_mat = np.array([[np.exp(-1j * z / 2), 0.0], [0.0, np.exp(1j * z / 2)]]) + + c = np.cos(x / 2) + s = np.sin(x / 2) * 1j + X_mat = np.array([[c, -s], [-s, c]]) + + qml.Hadamard(wires="a") + qml.QubitUnitary(Z_mat, wires="a") + qml.QubitUnitary(X_mat, wires="b") + qml.CNOT(wires=["b", "a"]) + return qml.expval(qml.PauliX(wires="a")) + ``` + + ```pycon + >>> dev = qml.device("default.qubit", wires=["a", "b"]) + >>> transformed_qfunc = qml.transforms.unitary_to_rot(qfunc_with_qubit_unitary) + >>> transformed_qnode = qml.QNode(transformed_qfunc, dev) + >>> input = np.array([0.3, 0.4], requires_grad=True) + >>> transformed_qnode(input) + tensor(0.95533649, requires_grad=True) + >>> qml.grad(transformed_qnode)(input) + array([-0.29552021, 0. ]) + ``` + * The new ``qml.apply`` function can be used to add operations that might have already been instantiated elsewhere to the QNode and other queuing contexts: [(#1433)](https://github.com/PennyLaneAI/pennylane/pull/1433) From 070bc97f9ad4a7ac9d978b9bd6f30db19c76a3ca Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Wed, 7 Jul 2021 09:55:43 -0400 Subject: [PATCH 37/37] Update CHANGELOG. --- .github/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2d2a9b8b74f..7c80c3c2879 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,7 +6,7 @@ single-qubit case fully differentiable in all interfaces. Furthermore, a quantum function transform, ``unitary_to_rot()``, has been added to decompose all single-qubit instances of ``QubitUnitary`` in a quantum circuit. - [(#1427)](https://github.com/PennyLaneAI/pennylane/pull/1427). + [(#1427)](https://github.com/PennyLaneAI/pennylane/pull/1427) Instances of ``QubitUnitary`` may now be decomposed directly to ``Rot`` operations, or ``RZ`` operations if the input matrix is diagonal. For