diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c94802240c2..0ba6054d119 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -61,6 +61,93 @@ [-0.3826, -0.1124]]]) ``` +- The TensorFlow interface now supports computing second derivatives and Hessians of hybrid quantum models. + Second derivatives are supported on both hardware and simulators. + [(#1110)](https://github.com/PennyLaneAI/pennylane/pull/1110) + + ```python + dev = qml.device('default.qubit', wires=1) + @qml.qnode(dev, interface='tf', diff_method='parameter-shift') + def circuit(x): + qml.RX(x[0], wires=0) + qml.RY(x[1], wires=0) + return qml.expval(qml.PauliZ(0)) + + x = tf.Variable([0.1, 0.2], dtype=tf.float64) + + with tf.GradientTape() as tape1: + with tf.GradientTape() as tape2: + y = circuit(x) + grad = tape2.gradient(res, x) + + hessian = tape1.jacobian(grad, x) + ``` + + To compute just the diagonal of the Hessian, the gradient of the + first derivatives can be taken: + + ```python + hessian_diagonals = tape1.gradient(grad, x) + ``` + +* Adds a new transform `qml.ctrl` that adds control wires to subroutines. + [(#1157)](https://github.com/PennyLaneAI/pennylane/pull/1157) + + Here's a simple usage example: + + ```python + def my_ansatz(params): + qml.RX(params[0], wires=0) + qml.RZ(params[1], wires=1) + + # Create a new method that applies `my_ansatz` + # controlled by the "2" wire. + my_anzats2 = qml.ctrl(my_ansatz, control=2) + + @qml.qnode(...) + def circuit(params): + my_ansatz2(params) + return qml.state() + ``` + + The above `circuit` would be equivalent to: + + ```python + @qml.qnode(...) + def circuit(params): + qml.CRX(params[0], wires=[2, 0]) + qml.CRZ(params[1], wires=[2, 1]) + return qml.state() + ``` + + The `qml.ctrl` transform is especially useful to repeatedly apply an + operation which is controlled by different qubits in each repetition. A famous example is Shor's algorithm. + + ```python + def modmul(a, mod, wires): + # Some complex set of gates that implements modular multiplcation. + # qml.CNOT(...); qml.Toffoli(...); ... + + @qml.qnode(...) + def shor(a, mod, scratch_wires, qft_wires): + for i, wire in enumerate(qft_wires): + qml.Hadamard(wire) + + # Create the controlled modular multiplication + # subroutine based on the control wire. + cmodmul = qml.ctrl(modmul, control=wire) + + # Execute the controlled modular multiplication. + cmodmul(a ** i, mod, scratch_wires) + + qml.adjoint(qml.QFT)(qft_wires) + return qml.sample() + + ``` + + In the future, devices will be able to exploit the sparsity of controlled operations to + improve simulation performance. + * Adds a new optimizer `qml.ShotAdaptiveOptimizer`, a gradient-descent optimizer where the shot rate is adaptively calculated using the variances of the parameter-shift gradient. [(#1139)](https://github.com/PennyLaneAI/pennylane/pull/1139) @@ -398,6 +485,32 @@

Improvements

+* Edited the ``MottonenStatePreparation`` template to improve performance on states with only real amplitudes + by reducing the number of redundant CNOT gates at the end of a circuit. + + ```python + dev = qml.device("default.qubit", wires=2) + + inputstate = [np.sqrt(0.2), np.sqrt(0.3), np.sqrt(0.4), np.sqrt(0.1)] + + @qml.qnode(dev) + def circuit(): + mottonen.MottonenStatePreparation(inputstate,wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + ``` + Previously returned: + ```pycon + >>> print(qml.draw(circuit)()) + 0: ──RY(1.57)──╭C─────────────╭C──╭C──╭C──┤ ⟨Z⟩ + 1: ──RY(1.35)──╰X──RY(0.422)──╰X──╰X──╰X──┤ + ``` + Now returns: + ```pycon + >>> print(qml.draw(circuit)()) + 0: ──RY(1.57)──╭C─────────────╭C──┤ ⟨Z⟩ + 1: ──RY(1.35)──╰X──RY(0.422)──╰X──┤ + ``` + - The `QAOAEmbedding` and `BasicEntanglerLayers` are now classes inheriting from `Operation`, and define the ansatz in their `expand()` method. This change does not affect the user interface. diff --git a/doc/code/qml_templates.rst b/doc/code/qml_templates.rst index 2eae7b9809f..74831b58070 100644 --- a/doc/code/qml_templates.rst +++ b/doc/code/qml_templates.rst @@ -77,4 +77,4 @@ Utility functions for quantum Monte Carlo .. automodapi:: pennylane.templates.subroutines.qmc :no-heading: :no-main-docstr: - :skip: QuantumMonteCarlo, template + :skip: QuantumMonteCarlo, Operation, Wires diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 08c81aa785a..ccb7af3b413 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -40,7 +40,9 @@ from pennylane.optimize import * from pennylane.qnode import QNode, qnode from pennylane.templates import broadcast, layer, template -from pennylane.transforms import adjoint, draw, measurement_grouping, metric_tensor +from pennylane.transforms import draw, measurement_grouping, metric_tensor +from pennylane.transforms.adjoint import adjoint +from pennylane.transforms.control import ctrl, ControlledOperation from pennylane.utils import inv from pennylane.vqe import ExpvalCost, Hamiltonian, VQECost diff --git a/pennylane/devices/default_qubit_jax.py b/pennylane/devices/default_qubit_jax.py index 9b0a4a8d54e..af8e707a8a9 100644 --- a/pennylane/devices/default_qubit_jax.py +++ b/pennylane/devices/default_qubit_jax.py @@ -142,6 +142,7 @@ def circuit(): "CRX": jax_ops.CRX, "CRY": jax_ops.CRY, "CRZ": jax_ops.CRZ, + "CRot": jax_ops.CRot, "MultiRZ": jax_ops.MultiRZ, "SingleExcitation": jax_ops.SingleExcitation, "SingleExcitationPlus": jax_ops.SingleExcitationPlus, diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index 1adcb55d38b..98dc115d0be 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -223,6 +223,9 @@ def pytest_generate_tests(metafunc): # translate command line string to None if necessary device_kwargs["shots"] = None if (opt.shots == "None") else int(opt.shots) + # store user defined device kwargs + device_kwargs.update(opt.device_kwargs) + list_of_device_kwargs.append(device_kwargs) # define the device_kwargs parametrization: diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 457f23296ab..286a7e8a73b 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -138,34 +138,88 @@ def _execute(self, params, **input_kwargs): res = self.execute_device(args, input_kwargs["device"]) self.set_parameters(all_params, trainable_only=False) - def grad(grad_output, **tfkwargs): - variables = tfkwargs.get("variables", None) + # The following dictionary caches the Jacobian and Hessian matrices, + # so that they can be re-used for different vjp/vhp computations + # within the same backpropagation call. + # This dictionary is tied to an instance of the inner function jacobian_product + # called within tf_tape.gradient or tf_tape.jacobian, + # via closure. Once tf_tape.gradient/ jacobian has returned, the jacobian_product instance + # will no longer be in scope and the memory will be freed. + saved_grad_matrices = {} + + def _evaluate_grad_matrix(grad_matrix_fn): + """Convenience function for generating gradient matrices + for the given parameter values. + + This function serves two purposes: + + * Avoids duplicating logic surrounding parameter unwrapping/wrapping + + * Takes advantage of closure, to cache computed gradient matrices via + the ``saved_grad_matrices`` dictionary, to avoid gradient matrices being + computed multiple redundant times. + + This is particularly useful when differentiating vector-valued QNodes. + Because tensorflow requests the vector-grad matrix product, + and *not* the full grad matrix, differentiating vector-valued + functions will result in multiple backward passes. + + Args: + grad_matrix_fn (str): Name of the gradient matrix function. Should correspond to an existing + tape method. Currently allowed values include ``"jacobian"`` and ``"hessian"``. + + Returns: + array[float]: the gradient matrix + """ + if grad_matrix_fn in saved_grad_matrices: + return saved_grad_matrices[grad_matrix_fn] self.set_parameters(all_params_unwrapped, trainable_only=False) - jacobian = self.jacobian(input_kwargs["device"], params=args, **self.jacobian_options) + grad_matrix = getattr(self, grad_matrix_fn)( + input_kwargs["device"], params=args, **self.jacobian_options + ) self.set_parameters(all_params, trainable_only=False) - jacobian = tf.constant(jacobian, dtype=self.dtype) + grad_matrix = tf.constant(grad_matrix, dtype=self.dtype) + saved_grad_matrices[grad_matrix_fn] = grad_matrix + + return grad_matrix + + def jacobian_product(dy, **tfkwargs): + variables = tfkwargs.get("variables", None) + dy_row = tf.reshape(dy, [1, -1]) + + @tf.custom_gradient + def jacobian(p): + def hessian_product(ddy, **tfkwargs): + variables = tfkwargs.get("variables", None) + hessian = _evaluate_grad_matrix("hessian") + + if self.output_dim == 1: + hessian = tf.expand_dims(hessian, -1) - # Reshape gradient output array as a 2D row-vector. - grad_output_row = tf.reshape(grad_output, [1, -1]) + vhp = tf.cond( + tf.rank(hessian) > 2, + lambda: dy_row @ ddy @ hessian @ tf.transpose(dy_row), + lambda: ddy @ hessian, + ) - # Calculate the vector-Jacobian matrix product, and unstack the output. - grad_input = tf.matmul(grad_output_row, jacobian) - grad_input = tf.unstack(tf.reshape(grad_input, [-1])) + vhp = tf.unstack(tf.reshape(vhp, [-1])) + return (vhp, variables) if variables is not None else vhp - if variables is not None: - return grad_input, variables + return _evaluate_grad_matrix("jacobian"), hessian_product - return grad_input + vjp = tf.matmul(dy_row, jacobian(params)) + vjp = tf.unstack(tf.reshape(vjp, [-1])) + return (vjp, variables) if variables is not None else vjp if self.is_sampled: - return res, grad + return res, jacobian_product if res.dtype == np.dtype("object"): res = np.hstack(res) - return tf.convert_to_tensor(res, dtype=self.dtype), grad + return tf.convert_to_tensor(res, dtype=self.dtype), jacobian_product @classmethod def apply(cls, tape, dtype=tf.float64): diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 161858fd22a..fa95a4af06f 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -94,6 +94,13 @@ def _evaluate_grad_matrix(grad_matrix_fn): Because PyTorch requests the vector-GradMatrix product, and *not* the full GradMatrix, differentiating vector-valued functions will result in multiple backward passes. + + Args: + grad_matrix_fn (str): Name of the gradient matrix function. Should correspond to an existing + tape method. Currently allowed values include ``"jacobian"`` and ``"hessian"``. + + Returns: + array[float]: the gradient matrix """ if grad_matrix_fn in ctx.saved_grad_matrices: return ctx.saved_grad_matrices[grad_matrix_fn] diff --git a/pennylane/operation.py b/pennylane/operation.py index 2498c24e95e..f0258517da4 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -623,7 +623,7 @@ def expand(self): operations decomposition, or if not implemented, simply the operation itself. """ - tape = qml.tape.QuantumTape() + tape = qml.tape.QuantumTape(do_queue=False) with tape: self.decomposition(*self.data, wires=self.wires) diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py index ff433e4477e..5ed613ba2c6 100644 --- a/pennylane/ops/qubit.py +++ b/pennylane/ops/qubit.py @@ -150,6 +150,9 @@ def decomposition(wires): def adjoint(self, do_queue=False): return PauliX(wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CNOT(wires=Wires(wire) + self.wires) + class PauliY(Observable, Operation): r"""PauliY(wires) @@ -207,6 +210,9 @@ def decomposition(wires): def adjoint(self, do_queue=False): return PauliY(wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CY(wires=Wires(wire) + self.wires) + class PauliZ(Observable, DiagonalOperation): r"""PauliZ(wires) @@ -247,6 +253,9 @@ def decomposition(wires): def adjoint(self, do_queue=False): return PauliZ(wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CZ(wires=Wires(wire) + self.wires) + class S(DiagonalOperation): r"""S(wires) @@ -400,6 +409,9 @@ def _matrix(cls, *params): def adjoint(self, do_queue=False): return CNOT(wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + Toffoli(wires=Wires(wire) + self.wires) + class CZ(DiagonalOperation): r"""CZ(wires) @@ -517,6 +529,9 @@ def _matrix(cls, *params): def adjoint(self, do_queue=False): return SWAP(wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CSWAP(wires=wire + self.wires) + class CSWAP(Operation): r"""CSWAP(wires) @@ -654,6 +669,9 @@ def _matrix(cls, *params): def adjoint(self, do_queue=False): return RX(-self.data[0], wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CRX(*self.parameters, wires=wire + self.wires) + class RY(Operation): r"""RY(phi, wires) @@ -692,6 +710,9 @@ def _matrix(cls, *params): def adjoint(self, do_queue=False): return RY(-self.data[0], wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CRY(*self.parameters, wires=wire + self.wires) + class RZ(DiagonalOperation): r"""RZ(phi, wires) @@ -736,6 +757,9 @@ def _eigvals(cls, *params): def adjoint(self, do_queue=False): return RZ(-self.data[0], wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CRZ(*self.parameters, wires=wire + self.wires) + class PhaseShift(DiagonalOperation): r"""PhaseShift(phi, wires) @@ -781,6 +805,9 @@ def decomposition(phi, wires): def adjoint(self, do_queue=False): return PhaseShift(-self.data[0], wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + ControlledPhaseShift(*self.parameters, wires=wire + self.wires) + class ControlledPhaseShift(DiagonalOperation): r"""ControlledPhaseShift(phi, wires) @@ -894,6 +921,9 @@ def adjoint(self, do_queue=False): phi, theta, omega = self.parameters return Rot(-omega, -theta, -phi, wires=self.wires, do_queue=do_queue) + def _controlled(self, wire): + CRot(*self.parameters, wires=wire + self.wires) + class MultiRZ(DiagonalOperation): r"""MultiRZ(theta, wires) @@ -1189,7 +1219,7 @@ class CRX(Operation): .. math:: \begin{align} - CR_x(\phi) &= + CR_x(\phi) &= \begin{bmatrix} & 1 & 0 & 0 & 0 \\ & 0 & 1 & 0 & 0\\ @@ -1248,7 +1278,7 @@ class CRY(Operation): .. math:: \begin{align} - CR_y(\phi) &= + CR_y(\phi) &= \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0\\ @@ -1305,7 +1335,7 @@ class CRZ(DiagonalOperation): .. math:: \begin{align} - CR_z(\phi) &= + CR_z(\phi) &= \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0\\ @@ -1539,8 +1569,10 @@ def decomposition(phi, lam, wires): return decomp_ops def adjoint(self, do_queue=False): - # TODO(chase): Replace the `inv()` by instead modifying the parameters. - return U2(*self.parameters, wires=self.wires, do_queue=do_queue).inv() + phi, lam = self.parameters + new_lam = (np.pi - phi) % (2 * np.pi) + new_phi = (np.pi - lam) % (2 * np.pi) + return U2(new_phi, new_lam, wires=self.wires, do_queue=do_queue) class U3(Operation): @@ -1606,8 +1638,10 @@ def decomposition(theta, phi, lam, wires): return decomp_ops def adjoint(self, do_queue=False): - # TODO(chase): Replace the `inv()` by instead modifying the parameters. - return U3(*self.parameters, wires=self.wires, do_queue=do_queue).inv() + theta, phi, lam = self.parameters + new_lam = (np.pi - phi) % (2 * np.pi) + new_phi = (np.pi - lam) % (2 * np.pi) + return U3(theta, new_phi, new_lam, wires=self.wires, do_queue=do_queue) # ============================================================================= @@ -1678,6 +1712,10 @@ def decomposition(theta, wires): ] return decomp_ops + def adjoint(self, do_queue=False): + (phi,) = self.parameters + return SingleExcitation(-phi, wires=self.wires, do_queue=do_queue) + class SingleExcitationMinus(Operation): r"""SingleExcitationMinus(phi, wires) @@ -1717,6 +1755,10 @@ def _matrix(cls, *params): return np.array([[e, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, e]]) + def adjoint(self, do_queue=False): + (phi,) = self.parameters + return SingleExcitationMinus(-phi, wires=self.wires, do_queue=do_queue) + class SingleExcitationPlus(Operation): r"""SingleExcitationPlus(phi, wires) @@ -1756,6 +1798,10 @@ def _matrix(cls, *params): return np.array([[e, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, e]]) + def adjoint(self, do_queue=False): + (phi,) = self.parameters + return SingleExcitationPlus(-phi, wires=self.wires, do_queue=do_queue) + # ============================================================================= # Arbitrary operations @@ -1798,6 +1844,9 @@ def adjoint(self, do_queue=False): qml.math.T(qml.math.conj(self.data[0])), wires=self.wires, do_queue=do_queue ) + def _controlled(self, wire): + ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires) + class ControlledQubitUnitary(QubitUnitary): r"""ControlledQubitUnitary(U, control_wires, wires, control_values) @@ -1892,7 +1941,7 @@ def _parse_control_values(control_wires, control_values): raise ValueError("Length of control bit string must equal number of control wires.") # Make sure all values are either 0 or 1 - if any([x not in ["0", "1"] for x in control_values]): + if any(x not in ["0", "1"] for x in control_values): raise ValueError("String of control values can contain only '0' or '1'.") control_int = int(control_values, 2) @@ -1901,6 +1950,9 @@ def _parse_control_values(control_wires, control_values): return control_int + def _controlled(self, wire): + ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires) + class MultiControlledX(ControlledQubitUnitary): r"""MultiControlledX(control_wires, wires, control_values) @@ -1981,7 +2033,15 @@ def decomposition(D, wires): return [QubitUnitary(np.diag(D), wires=wires)] def adjoint(self, do_queue=False): - return DiagonalQubitUnitary(self.parameters[0].conj(), wires=self.wires, do_queue=do_queue) + return DiagonalQubitUnitary( + qml.math.conj(self.parameters[0]), wires=self.wires, do_queue=do_queue + ) + + def _controlled(self, control): + DiagonalQubitUnitary( + qml.math.concatenate([np.array([1, 1]), self.parameters[0]]), + wires=Wires(control) + self.wires, + ) class QFT(Operation): @@ -2155,6 +2215,10 @@ def decomposition(theta, wires): ] return decomp_ops + def adjoint(self, do_queue=False): + (theta,) = self.parameters + return DoubleExcitation(-theta, wires=self.wires, do_queue=do_queue) + class DoubleExcitationPlus(Operation): r"""DoubleExcitationPlus(phi, wires) @@ -2211,6 +2275,10 @@ def _matrix(cls, *params): return U + def adjoint(self, do_queue=False): + (theta,) = self.parameters + return DoubleExcitationPlus(-theta, wires=self.wires, do_queue=do_queue) + class DoubleExcitationMinus(Operation): r"""DoubleExcitationMinus(phi, wires) @@ -2267,6 +2335,10 @@ def _matrix(cls, *params): return U + def adjoint(self, do_queue=False): + (theta,) = self.parameters + return DoubleExcitationMinus(-theta, wires=self.wires, do_queue=do_queue) + # ============================================================================= # State preparation diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 2c944fa36df..8a8d689c74b 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -311,7 +311,7 @@ def compute_output_shape(self, input_shape): Returns: tf.TensorShape: shape of output data """ - return tf.TensorShape(input_shape[0]).concatenate(self.output_dim) + return tf.TensorShape([input_shape[0]]).concatenate(self.output_dim) def __str__(self): detail = "" diff --git a/pennylane/tape/jacobian_tape.py b/pennylane/tape/jacobian_tape.py index c4adc6934fb..62eddcfc9a9 100644 --- a/pennylane/tape/jacobian_tape.py +++ b/pennylane/tape/jacobian_tape.py @@ -490,7 +490,7 @@ def jacobian(self, device, params=None, **options): >>> tape.jacobian(dev) array([], shape=(4, 0), dtype=float64) """ - if any([m.return_type is State for m in self.measurements]): + if any(m.return_type is State for m in self.measurements): raise ValueError("The jacobian method does not support circuits that return the state") if self.is_sampled: @@ -664,7 +664,7 @@ def hessian(self, device, params=None, **options): >>> tape.hessian(dev) array([], shape=(0, 0), dtype=float64) """ - if any([m.return_type is State for m in self.measurements]): + if any(m.return_type is State for m in self.measurements): raise ValueError("The Hessian method does not support circuits that return the state") method = options.get("method", "analytic") diff --git a/pennylane/templates/state_preparations/mottonen.py b/pennylane/templates/state_preparations/mottonen.py index d61aee72b52..17b43c3a43a 100644 --- a/pennylane/templates/state_preparations/mottonen.py +++ b/pennylane/templates/state_preparations/mottonen.py @@ -293,10 +293,11 @@ def MottonenStatePreparation(state_vector, wires): target = wires_reverse[k - 1] _uniform_rotation_dagger(qml.RY, alpha_y_k, control, target) - # Apply inverse z rotation cascade to prepare correct phases of amplitudes - for k in range(len(wires_reverse), 0, -1): - alpha_z_k = _get_alpha_z(omega, len(wires_reverse), k) - control = wires_reverse[k:] - target = wires_reverse[k - 1] - if len(alpha_z_k) > 0: - _uniform_rotation_dagger(qml.RZ, alpha_z_k, control, target) + # If necessary, apply inverse z rotation cascade to prepare correct phases of amplitudes + if not qml.math.allclose(omega, 0): + for k in range(len(wires_reverse), 0, -1): + alpha_z_k = _get_alpha_z(omega, len(wires_reverse), k) + control = wires_reverse[k:] + target = wires_reverse[k - 1] + if len(alpha_z_k) > 0: + _uniform_rotation_dagger(qml.RZ, alpha_z_k, control, target) diff --git a/pennylane/templates/subroutines/qmc.py b/pennylane/templates/subroutines/qmc.py index 9f0dc7f139b..a4cd4c86848 100644 --- a/pennylane/templates/subroutines/qmc.py +++ b/pennylane/templates/subroutines/qmc.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Contains the ``QuantumMonteCarlo`` template and utility functions. +Contains the QuantumMonteCarlo template and utility functions. """ import numpy as np import pennylane as qml -from pennylane.templates.decorator import template +from pennylane.operation import AnyWires, Operation from pennylane.wires import Wires @@ -202,8 +202,7 @@ def make_Q(A, R): return UV @ UV -@template -def QuantumMonteCarlo(probs, func, target_wires, estimation_wires): +class QuantumMonteCarlo(Operation): r"""Performs the `quantum Monte Carlo estimation `__ algorithm. @@ -270,8 +269,9 @@ def QuantumMonteCarlo(probs, func, target_wires, estimation_wires): .. note:: This template is only compatible with simulators because the algorithm is performed using - unitary matrices. To implement the quantum Monte Carlo algorithm on hardware requires - breaking down the unitary matrices into hardware-compatible gates. + unitary matrices. Additionally, this operation is not differentiable. To implement the + quantum Monte Carlo algorithm on hardware requires breaking down the unitary matrices into + hardware-compatible gates. .. UsageDetails:: @@ -326,32 +326,46 @@ def circuit(): >>> (1 - np.cos(np.pi * phase_estimated)) / 2 0.4327096457464369 """ - if isinstance(probs, np.ndarray) and probs.ndim != 1: - raise ValueError("The probability distribution must be specified as a flat array") - - dim_p = len(probs) - num_target_wires = float(np.log2(2 * dim_p)) - - if not num_target_wires.is_integer(): - raise ValueError("The probability distribution must have a length that is a power of two") - - num_target_wires = int(num_target_wires) - - target_wires = Wires(target_wires) - estimation_wires = Wires(estimation_wires) - - if num_target_wires != len(target_wires): - raise ValueError( - f"The probability distribution of dimension {dim_p} requires" - f" {num_target_wires} target wires" - ) - - A = probs_to_unitary(probs) - R = func_to_unitary(func, dim_p) - Q = make_Q(A, R) - - qml.QubitUnitary(A, wires=target_wires[:-1]) - qml.QubitUnitary(R, wires=target_wires) - qml.templates.QuantumPhaseEstimation( - Q, target_wires=target_wires, estimation_wires=estimation_wires - ) + num_params = 3 + num_wires = AnyWires + par_domain = "A" + + def __init__(self, probs, func, target_wires, estimation_wires, do_queue=True): + if isinstance(probs, np.ndarray) and probs.ndim != 1: + raise ValueError("The probability distribution must be specified as a flat array") + + dim_p = len(probs) + num_target_wires_ = np.log2(2 * dim_p) + num_target_wires = int(num_target_wires_) + + if not np.allclose(num_target_wires_, num_target_wires): + raise ValueError( + "The probability distribution must have a length that is a power of two" + ) + + self.target_wires = Wires(target_wires) + self.estimation_wires = Wires(estimation_wires) + wires = self.target_wires + self.estimation_wires + + if num_target_wires != len(self.target_wires): + raise ValueError( + f"The probability distribution of dimension {dim_p} requires" + f" {num_target_wires} target wires" + ) + + A = probs_to_unitary(probs) + R = func_to_unitary(func, dim_p) + Q = make_Q(A, R) + super().__init__(A, R, Q, wires=wires, do_queue=do_queue) + + def expand(self): + A, R, Q = self.parameters + + with qml.tape.QuantumTape() as tape: + qml.QubitUnitary(A, wires=self.target_wires[:-1]) + qml.QubitUnitary(R, wires=self.target_wires) + qml.templates.QuantumPhaseEstimation( + Q, target_wires=self.target_wires, estimation_wires=self.estimation_wires + ) + + return tape diff --git a/pennylane/templates/subroutines/qpe.py b/pennylane/templates/subroutines/qpe.py index b83544bc3a0..6e19f56c816 100644 --- a/pennylane/templates/subroutines/qpe.py +++ b/pennylane/templates/subroutines/qpe.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Contains the ``QuantumPhaseEstimation`` template. +Contains the QuantumPhaseEstimation template. """ import pennylane as qml -from pennylane.templates.decorator import template +from pennylane.operation import AnyWires, Operation from pennylane.wires import Wires -@template -def QuantumPhaseEstimation(unitary, target_wires, estimation_wires): +class QuantumPhaseEstimation(Operation): r"""Performs the `quantum phase estimation `__ circuit. @@ -104,21 +103,38 @@ def circuit(): # Need to rescale phase due to convention of RX gate phase_estimated = 4 * np.pi * (1 - phase_estimated) """ + num_params = 1 + num_wires = AnyWires + par_domain = "A" - target_wires = Wires(target_wires) - estimation_wires = Wires(estimation_wires) + def __init__(self, unitary, target_wires, estimation_wires, do_queue=True): + self.target_wires = Wires(target_wires) + self.estimation_wires = Wires(estimation_wires) - if len(Wires.shared_wires([target_wires, estimation_wires])) != 0: - raise qml.QuantumFunctionError("The target wires and estimation wires must be different") + wires = self.target_wires + self.estimation_wires - unitary_powers = [unitary] + if len(Wires.shared_wires([self.target_wires, self.estimation_wires])) != 0: + raise qml.QuantumFunctionError( + "The target wires and estimation wires must be different" + ) - for _ in range(len(estimation_wires) - 1): - new_power = unitary_powers[-1] @ unitary_powers[-1] - unitary_powers.append(new_power) + super().__init__(unitary, wires=wires, do_queue=do_queue) - for wire in estimation_wires: - qml.Hadamard(wire) - qml.ControlledQubitUnitary(unitary_powers.pop(), control_wires=wire, wires=target_wires) + def expand(self): + unitary = self.parameters[0] + unitary_powers = [unitary] - qml.QFT(wires=estimation_wires).inv() + for _ in range(len(self.estimation_wires) - 1): + new_power = unitary_powers[-1] @ unitary_powers[-1] + unitary_powers.append(new_power) + + with qml.tape.QuantumTape() as tape: + for wire in self.estimation_wires: + qml.Hadamard(wire) + qml.ControlledQubitUnitary( + unitary_powers.pop(), control_wires=wire, wires=self.target_wires + ) + + qml.QFT(wires=self.estimation_wires).inv() + + return tape diff --git a/pennylane/transforms/control.py b/pennylane/transforms/control.py new file mode 100644 index 00000000000..a651835e6c7 --- /dev/null +++ b/pennylane/transforms/control.py @@ -0,0 +1,183 @@ +# 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 the control transform. +""" +from functools import wraps +from pennylane.tape import QuantumTape +from pennylane.operation import Operation, AnyWires +from pennylane.wires import Wires +from pennylane.transforms.adjoint import adjoint + + +def requeue_ops_in_tape(tape): + """Requeue all of the operations in a tape directly to the current tape context""" + for op in tape.operations: + op.queue() + + +def expand_with_control(tape, control_wire): + """Expand a tape to include a control wire on all queued operations. + + Args: + tape (.QuantumTape): quantum tape to be controlled + control_wire (int): a single wire to use as the control wire + + Returns: + .QuantumTape: A new QuantumTape with the controlled operations. + """ + with QuantumTape(do_queue=False) as new_tape: + for op in tape.operations: + if hasattr(op, "_controlled"): + # Execute the controlled version of the operation + # and add that the to the tape context. + # pylint: disable=protected-access + op._controlled(control_wire) + else: + tmp_tape = op.expand() + tmp_tape = expand_with_control(tmp_tape, control_wire) + requeue_ops_in_tape(tmp_tape) + return new_tape + + +class ControlledOperation(Operation): + """A Controlled Operation. + + Unless you are a Pennylane plugin developer, **you should NOT directly use this class**, + instead, use the :func:`qml.ctrl <.ctrl>` function. + + The ``ControlledOperation`` class is a container class that defines a set of operations that + should by applied relative to a single control wire or a list of control wires. + + Certain simulators and quantum computers can take advantage of the controlled gate sparsity, + while other devices must rely on the op-by-op decomposition defined by the ``op.expand`` + method. + + Args: + tape: A QuantumTape. This tape defines the unitary that should be applied relative + to the control wires. + control_wires: A wire or set of wires. + """ + + par_domain = "A" + num_wires = AnyWires + num_params = property(lambda self: self.subtape.num_params) + + def __init__(self, tape, control_wires, do_queue=True): + self.subtape = tape + """QuantumTape: The tape that defines the underlying operation.""" + + self.control_wires = Wires(control_wires) + """Wires: The control wires.""" + + wires = self.control_wires + tape.wires + super().__init__(*tape.get_parameters(), wires=wires, do_queue=do_queue) + + def expand(self): + tape = self.subtape + for wire in self.control_wires: + tape = expand_with_control(tape, wire) + return tape + + def adjoint(self, do_queue=False): + with QuantumTape(do_queue=False) as new_tape: + # Execute all ops adjointed. + adjoint(requeue_ops_in_tape)(self.subtape) + return ControlledOperation(new_tape, self.control_wires, do_queue=do_queue) + + def _controlled(self, wires): + ControlledOperation(tape=self.subtape, control_wires=Wires(wires) + self.control_wires) + + +def ctrl(fn, control): + """Create a method that applies a controlled version of the provided method. + + Args: + fn (function): Any python function that applies pennylane operations. + control (Wires): The control wire(s). + + Returns: + function: A new function that applies the controlled equivalent of ``fn``. The returned + function takes the same input arguments as ``fn``. + + **Example** + + .. code-block:: python3 + + def ops(params): + qml.RX(params[0], wires=0) + qml.RZ(params[1] wires=3) + + ops1 = qml.ctrl(ops, control=1) + ops2 = qml.ctrl(ops, control=2) + + @qml.qnode(dev) + def my_circuit(): + ops1(params=[0.123, 0.456]) + ops1(params=[0.789, 1.234]) + ops2(params=[2.987, 3.654]) + ops2(params=[2.321, 1.111]) + return qml.state() + + The above code would be equivalent to + + .. code-block:: python3 + + @qml.qnode(dev) + def my_circuit(params): + # ops1(params=[0.123, 0.456]) + qml.CRX(0.123, wires=[1, 0]) + qml.CRZ(0.456, wires=[1, 3]) + + # ops1(params=[0.789, 1.234]) + qml.CRX(0.789, wires=[1, 0]) + qml.CRZ(1.234, (wires=[1, 3]) + + # ops2(params=[2.987, 3.654]) + qml.CRX(2.987, wires=[2, 0]) + qml.CRZ(3.654, wires=[2, 3]) + + # ops2(params=[2.321, 1.111]) + qml.CRX(2.321, wires=[2, 0]) + qml.CRZ(1.111, wires=[2, 3]) + return qml.state() + + .. Note:: + + Some devices are able to take advantage of the inherient sparsity of a + controlled operation. In those cases, it may be more efficient to use + this transform rather than adding controls by hand. For devices that don't + have special control support, the operation is expanded to add control wires + to each underlying op individually. + + .. UsageDetails:: + + **Nesting Controls** + + The ``ctrl`` transform can be nested with itself arbitrarily. + + .. code-block:: python3 + + # These two ops are equivalent. + op1 = qml.ctrl(qml.ctrl(my_ops, 1), 2) + op2 = qml.ctrl(my_ops, [2, 1]) + """ + + @wraps(fn) + def wrapper(*args, **kwargs): + with QuantumTape(do_queue=False) as tape: + fn(*args, **kwargs) + return ControlledOperation(tape, control) + + return wrapper diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index 512562324a3..31616a8a2f1 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -161,6 +161,9 @@ def test_gates_dont_crash(self): @qml.qnode(dev, interface="jax", diff_method="backprop") def circuit(): qml.CRZ(0.0, wires=[0, 1]) + qml.CRX(0.0, wires=[0, 1]) + qml.PhaseShift(0.0, wires=0) + qml.ControlledPhaseShift(0.0, wires=[1, 0]) qml.CRot(1.0, 0.0, 0.0, wires=[0, 1]) qml.CRY(0.0, wires=[0, 1]) return qml.sample(qml.PauliZ(wires=0)) @@ -259,7 +262,7 @@ def circuit(a): def cost(a): """A function of the device quantum state, as a function - of ijnput QNode parameters.""" + of input QNode parameters.""" res = jnp.abs(circuit(a)) ** 2 return res[1] - res[0] @@ -267,6 +270,31 @@ def cost(a): expected = jnp.sin(a) assert jnp.allclose(grad, expected, atol=tol, rtol=0) +@pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, np.pi, 7)) +def test_CRot_gradient(theta, tol): + """Tests that the automatic gradient of a arbitrary controlled Euler-angle-parameterized + gate is correct.""" + dev = qml.device("default.qubit.jax", wires=2) + a, b, c = np.array([theta, theta ** 3, np.sqrt(2) * theta]) + + @qml.qnode(dev, diff_method="backprop", interface="jax") + def circuit(a, b, c): + qml.QubitStateVector(np.array([1., -1.]) / np.sqrt(2), wires=0) + qml.CRot(a, b, c, wires=[0, 1]) + return qml.expval(qml.PauliX(0)) + + res = circuit(a, b, c) + expected = -np.cos(b / 2) * np.cos(0.5 * (a + c)) + assert np.allclose(res, expected, atol=tol, rtol=0) + + grad = jax.grad(circuit, argnums=(0, 1, 2))(a, b, c) + expected = np.array([[ + 0.5 * np.cos(b / 2) * np.sin(0.5 * (a + c)), + 0.5 * np.sin(b / 2) * np.cos(0.5 * (a + c)), + 0.5 * np.cos(b / 2) * np.sin(0.5 * (a + c)), + ]]) + assert np.allclose(grad, expected, atol=tol, rtol=0) + def test_prob_differentiability(self, tol): """Test that the device probability can be differentiated""" dev = qml.device("default.qubit.jax", wires=2) diff --git a/tests/interfaces/test_qnode_tf.py b/tests/interfaces/test_qnode_tf.py index 475a30a3acc..45bcca3a29f 100644 --- a/tests/interfaces/test_qnode_tf.py +++ b/tests/interfaces/test_qnode_tf.py @@ -32,7 +32,7 @@ ], ) class TestQNode: - """Same tests as above, but this time via the QNode interface!""" + """Tests the tensorflow interface used with a QNode.""" def test_execution_no_interface(self, dev_name, diff_method): """Test execution works without an interface, and that trainable parameters @@ -525,6 +525,208 @@ def circuit(): assert res.shape == (2, 10) assert isinstance(res, tf.Tensor) + def test_second_derivative(self, dev_name, diff_method, mocker, tol): + """Test second derivative calculation of a scalar valued QNode""" + if diff_method not in {"parameter-shift", "backprop"}: + pytest.skip("Test only supports parameter-shift or backprop") + + dev = qml.device(dev_name, wires=1) + + @qnode(dev, diff_method=diff_method, interface="tf") + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.PauliZ(0)) + + x = tf.Variable([1.0, 2.0], dtype=tf.float64) + + with tf.GradientTape() as tape1: + with tf.GradientTape() as tape2: + res = circuit(x) + g = tape2.gradient(res, x) + res2 = tf.reduce_sum(g) + + spy = mocker.spy(JacobianTape, "hessian") + g2 = tape1.gradient(res2, x) + + if diff_method == "parameter-shift": + spy.assert_called_once() + elif diff_method == "backprop": + spy.assert_not_called() + + a, b = x * 1.0 + + expected_res = tf.cos(a) * tf.cos(b) + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + expected_g = [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + expected_g2 = [-tf.cos(a) * tf.cos(b) + tf.sin(a) * tf.sin(b), tf.sin(a) * tf.sin(b) - tf.cos(a) * tf.cos(b)] + assert np.allclose(g2, expected_g2, atol=tol, rtol=0) + + def test_hessian(self, dev_name, diff_method, mocker, tol): + """Test hessian calculation of a scalar valued QNode""" + if diff_method not in {"parameter-shift", "backprop"}: + pytest.skip("Test only supports parameter-shift or backprop") + + dev = qml.device(dev_name, wires=1) + + @qnode(dev, diff_method=diff_method, interface="tf") + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.expval(qml.PauliZ(0)) + + x = tf.Variable([1.0, 2.0], dtype=tf.float64) + + with tf.GradientTape() as tape1: + with tf.GradientTape() as tape2: + res = circuit(x) + g = tape2.gradient(res, x) + + spy = mocker.spy(JacobianTape, "hessian") + hess = tape1.jacobian(g, x) + + if diff_method == "parameter-shift": + spy.assert_called_once() + elif diff_method == "backprop": + spy.assert_not_called() + + a, b = x * 1.0 + + expected_res = tf.cos(a) * tf.cos(b) + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + expected_g = [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + expected_hess = [ + [-tf.cos(a) * tf.cos(b), tf.sin(a) * tf.sin(b)], + [tf.sin(a) * tf.sin(b), -tf.cos(a) * tf.cos(b)] + ] + assert np.allclose(hess, expected_hess, atol=tol, rtol=0) + + def test_hessian_vector_valued(self, dev_name, diff_method, mocker, tol): + """Test hessian calculation of a vector valued QNode""" + if diff_method not in {"parameter-shift", "backprop"}: + pytest.skip("Test only supports parameter-shift or backprop") + + dev = qml.device(dev_name, wires=1) + + @qnode(dev, diff_method=diff_method, interface="tf") + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + return qml.probs(wires=0) + + x = tf.Variable([1.0, 2.0], dtype=tf.float64) + + with tf.GradientTape(persistent=True) as tape1: + with tf.GradientTape(persistent=True) as tape2: + res = circuit(x) + + spy = mocker.spy(JacobianTape, "hessian") + g = tape2.jacobian(res, x, experimental_use_pfor=False) + + hess = tape1.jacobian(g, x, experimental_use_pfor=False) + + if diff_method == "parameter-shift": + spy.assert_called_once() + elif diff_method == "backprop": + spy.assert_not_called() + + a, b = x * 1.0 + + expected_res = [ + 0.5 + 0.5 * tf.cos(a) * tf.cos(b), + 0.5 - 0.5 * tf.cos(a) * tf.cos(b) + ] + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + expected_g = [ + [-0.5 * tf.sin(a) * tf.cos(b), -0.5 * tf.cos(a) * tf.sin(b)], + [0.5 * tf.sin(a) * tf.cos(b), 0.5 * tf.cos(a) * tf.sin(b)] + ] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + expected_hess = [ + [ + [-0.5 * tf.cos(a) * tf.cos(b), 0.5 * tf.sin(a) * tf.sin(b)], + [0.5 * tf.sin(a) * tf.sin(b), -0.5 * tf.cos(a) * tf.cos(b)] + ], + [ + [0.5 * tf.cos(a) * tf.cos(b), -0.5 * tf.sin(a) * tf.sin(b)], + [-0.5 * tf.sin(a) * tf.sin(b), 0.5 * tf.cos(a) * tf.cos(b)] + ] + ] + + np.testing.assert_allclose(hess, expected_hess, atol=tol, rtol=0, verbose=True) + + def test_hessian_ragged(self, dev_name, diff_method, mocker, tol): + """Test hessian calculation of a ragged QNode""" + if diff_method not in {"parameter-shift", "backprop"}: + pytest.skip("Test only supports parameter-shift or backprop") + + dev = qml.device(dev_name, wires=2) + + @qnode(dev, diff_method=diff_method, interface="tf") + def circuit(x): + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + qml.RY(x[0], wires=1) + qml.RX(x[1], wires=1) + return qml.expval(qml.PauliZ(0)), qml.probs(wires=1) + + x = tf.Variable([1.0, 2.0], dtype=tf.float64) + res = circuit(x) + + with tf.GradientTape(persistent=True) as tape1: + with tf.GradientTape(persistent=True) as tape2: + res = circuit(x) + + spy = mocker.spy(JacobianTape, "hessian") + g = tape2.jacobian(res, x, experimental_use_pfor=False) + + hess = tape1.jacobian(g, x, experimental_use_pfor=False) + + if diff_method == "parameter-shift": + spy.assert_called_once() + elif diff_method == "backprop": + spy.assert_not_called() + + a, b = x * 1.0 + + expected_res = [ + tf.cos(a) * tf.cos(b), + 0.5 + 0.5 * tf.cos(a) * tf.cos(b), + 0.5 - 0.5 * tf.cos(a) * tf.cos(b) + ] + assert np.allclose(res, expected_res, atol=tol, rtol=0) + + expected_g = [ + [-tf.sin(a) * tf.cos(b), -tf.cos(a) * tf.sin(b)], + [-0.5 * tf.sin(a) * tf.cos(b), -0.5 * tf.cos(a) * tf.sin(b)], + [0.5 * tf.sin(a) * tf.cos(b), 0.5 * tf.cos(a) * tf.sin(b)] + ] + assert np.allclose(g, expected_g, atol=tol, rtol=0) + + expected_hess = [ + [ + [-tf.cos(a) * tf.cos(b), tf.sin(a) * tf.sin(b)], + [tf.sin(a) * tf.sin(b), -tf.cos(a) * tf.cos(b)] + ], + [ + [-0.5 * tf.cos(a) * tf.cos(b), 0.5 * tf.sin(a) * tf.sin(b)], + [0.5 * tf.sin(a) * tf.sin(b), -0.5 * tf.cos(a) * tf.cos(b)] + ], + [ + [0.5 * tf.cos(a) * tf.cos(b), -0.5 * tf.sin(a) * tf.sin(b)], + [-0.5 * tf.sin(a) * tf.sin(b), 0.5 * tf.cos(a) * tf.cos(b)] + ] + ] + np.testing.assert_allclose(hess, expected_hess, atol=tol, rtol=0, verbose=True) + def qtransform(qnode, a, framework=tf): """Transforms every RY(y) gate in a circuit to RX(-a*cos(y))""" diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py index 009f74e0cbb..24d24464922 100644 --- a/tests/ops/test_qubit_ops.py +++ b/tests/ops/test_qubit_ops.py @@ -369,6 +369,12 @@ def test_matrices(self, ops, mat, tol): qml.QFT(wires=[1, 2, 3]), qml.ControlledQubitUnitary(np.eye(2) * 1j, wires=[0], control_wires=[2]), qml.MultiControlledX(control_wires=[0, 1], wires=2, control_values='01'), + qml.SingleExcitation(0.123, wires=[0, 3]), + qml.SingleExcitationPlus(0.123, wires=[0, 3]), + qml.SingleExcitationMinus(0.123, wires=[0, 3]), + qml.DoubleExcitation(0.123, wires=[0, 1, 2, 3]), + qml.DoubleExcitationPlus(0.123, wires=[0, 1, 2, 3]), + qml.DoubleExcitationMinus(0.123, wires=[0, 1, 2, 3]), ]) def test_adjoint_unitaries(self, op, tol): op_d = op.adjoint() diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index b5ab2293858..cc4798e058f 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -485,6 +485,18 @@ def f(inputs, weights): assert grad is not None spy.assert_not_called() + @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) + def test_compute_output_shape(self, get_circuit, output_dim): + """Test that the compute_output_shape method returns the expected shape""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + + inputs = tf.keras.Input(shape=(2,)) + inputs_shape = inputs.shape + + output_shape = layer.compute_output_shape(inputs_shape) + assert output_shape.as_list() == [None, 1] + @pytest.mark.parametrize("interface", ["autograd", "torch", "tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices_up_to(1)) diff --git a/tests/tape/test_qubit_param_shift.py b/tests/tape/test_qubit_param_shift.py index 045c6538945..89097ee9d87 100644 --- a/tests/tape/test_qubit_param_shift.py +++ b/tests/tape/test_qubit_param_shift.py @@ -579,6 +579,33 @@ def test_pauli_rotation_hessian(self, s1, s2, G, tol): assert np.allclose(autograd_val, manualgrad_val, atol=tol, rtol=0) + def test_vector_output(self, tol): + """Tests that a vector valued output tape has a hessian with the proper result. """ + + dev = qml.device('default.qubit', wires=1) + + x = np.array([1.0, 2.0]) + + with QubitParamShiftTape() as tape: + qml.RY(x[0], wires=0) + qml.RX(x[1], wires=0) + qml.probs(wires=[0]) + + hess = tape.hessian(dev) + + expected_hess = expected_hess = np.array([ + [ + [-0.5 * np.cos(x[0]) * np.cos(x[1]), 0.5 * np.cos(x[0]) * np.cos(x[1])], + [ 0.5 * np.sin(x[0]) * np.sin(x[1]), -0.5 * np.sin(x[0]) * np.sin(x[1])] + ], + [ + [0.5 * np.sin(x[0]) * np.sin(x[1]), -0.5 * np.sin(x[0]) * np.sin(x[1])], + [-0.5 * np.cos(x[0]) * np.cos(x[1]), 0.5 * np.cos(x[0]) * np.cos(x[1])] + ] + ]) + + assert np.allclose(hess, expected_hess, atol=tol, rtol=0) + def test_no_trainable_params_hessian(self): """Test that an empty Hessian is returned when there are no trainable parameters.""" diff --git a/tests/templates/test_state_preparations.py b/tests/templates/test_state_preparations.py index e8ef54d96c6..fe5a8dc5426 100644 --- a/tests/templates/test_state_preparations.py +++ b/tests/templates/test_state_preparations.py @@ -19,14 +19,18 @@ import math from unittest.mock import patch -import numpy as np +from pennylane import numpy as np import pytest import pennylane as qml -from pennylane.templates.state_preparations import (BasisStatePreparation, - MottonenStatePreparation, - ArbitraryStatePreparation) +from pennylane.templates.state_preparations import ( + BasisStatePreparation, + MottonenStatePreparation, + ArbitraryStatePreparation, +) from pennylane.templates.state_preparations.mottonen import gray_code -from pennylane.templates.state_preparations.arbitrary_state_preparation import _state_preparation_pauli_words +from pennylane.templates.state_preparations.arbitrary_state_preparation import ( + _state_preparation_pauli_words, +) from pennylane.templates.state_preparations.mottonen import _get_alpha_y from pennylane.wires import Wires @@ -47,11 +51,32 @@ def test_gray_code(self, rank, expected_gray_code): assert gray_code(rank) == expected_gray_code - @pytest.mark.parametrize("num_wires,expected_pauli_words", [ - (1, ["X", "Y"]), - (2, ["XI", "YI", "IX", "IY", "XX", "XY"]), - (3, ["XII", "YII", "IXI", "IYI", "IIX", "IIY", "IXX", "IXY", "XXI", "XYI", "XIX", "XIY", "XXX", "XXY"]), - ]) + @pytest.mark.parametrize( + "num_wires,expected_pauli_words", + [ + (1, ["X", "Y"]), + (2, ["XI", "YI", "IX", "IY", "XX", "XY"]), + ( + 3, + [ + "XII", + "YII", + "IXI", + "IYI", + "IIX", + "IIY", + "IXX", + "IXY", + "XXI", + "XYI", + "XIX", + "XIY", + "XXX", + "XXY", + ], + ), + ], + ) def test_state_preparation_pauli_words(self, num_wires, expected_pauli_words): """Test that the correct Pauli words are returned.""" for idx, pauli_word in enumerate(_state_preparation_pauli_words(num_wires)): @@ -214,7 +239,9 @@ class TestMottonenStatePreparation: ([1/2, 0, 1j/2, 1j/math.sqrt(2)], [0, 1], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ]) # fmt: on - def test_state_preparation_fidelity(self, tol, qubit_device_3_wires, state_vector, wires, target_state): + def test_state_preparation_fidelity( + self, tol, qubit_device_3_wires, state_vector, wires, target_state + ): """Tests that the template MottonenStatePreparation integrates correctly with PennyLane and produces states with correct fidelity.""" @@ -227,7 +254,7 @@ def circuit(): circuit() state = circuit.device.state.ravel() - fidelity = abs(np.vdot(state, target_state))**2 + fidelity = abs(np.vdot(state, target_state)) ** 2 # We test for fidelity here, because the vector themselves will hardly match # due to imperfect state preparation @@ -278,7 +305,9 @@ def circuit(): ([1/2, 0, 1j/2, 1j/math.sqrt(2)], [0, 1], [1/2, 0, 0, 0, 1j/2, 0, 1j/math.sqrt(2), 0]), ]) # fmt: on - def test_state_preparation_probability_distribution(self, tol, qubit_device_3_wires, state_vector, wires, target_state): + def test_state_preparation_probability_distribution( + self, tol, qubit_device_3_wires, state_vector, wires, target_state + ): """Tests that the template MottonenStatePreparation integrates correctly with PennyLane and produces states with correct probability distribution.""" @@ -292,8 +321,8 @@ def circuit(): state = circuit.device.state.ravel() - probabilities = np.abs(state)**2 - target_probabilities = np.abs(target_state)**2 + probabilities = np.abs(state) ** 2 + target_probabilities = np.abs(target_state) ** 2 assert np.allclose(probabilities, target_probabilities, atol=tol, rtol=0) @@ -324,11 +353,14 @@ def test_error_num_entries(self, state_vector, wires): with pytest.raises(ValueError, match="State vector must be of (length|shape)"): MottonenStatePreparation(state_vector, wires) - @pytest.mark.parametrize("current_qubit, expected", [ - (1, np.array([0, 0, 0, 1.23095942])), - (2, np.array([2.01370737, 3.14159265])), - (3, np.array([1.15927948])), - ]) + @pytest.mark.parametrize( + "current_qubit, expected", + [ + (1, np.array([0, 0, 0, 1.23095942])), + (2, np.array([2.01370737, 3.14159265])), + (3, np.array([1.15927948])), + ], + ) def test_get_alpha_y(self, current_qubit, expected, tol): """Test the _get_alpha_y helper function.""" @@ -359,6 +391,52 @@ def circuit(state_vector): state_vector = np.array([0, 2, 0, 0]) circuit(state_vector) + # fmt: off + @pytest.mark.parametrize("state_vector, n_wires", [ + ([1/2, 1/2, 1/2, 1/2], 2), + ([1, 0, 0, 0], 2), + ([0, 1, 0, 0], 2), + ([0, 0, 0, 1], 2), + ([0, 1, 0, 0, 0, 0, 0, 0], 3), + ([0, 0, 0, 0, 1, 0, 0, 0], 3), + ([2/3, 0, 0, 0, 1/3, 0, 0, 2/3], 3), + ([1/2, 0, 0, 0, 1/2, 1/2, 1/2, 0], 3), + ([1/3, 0, 0, 0, 2/3, 2/3, 0, 0], 3), + ([2/3, 0, 0, 0, 1/3, 0, 0, 2/3], 3), + ]) + # fmt: on + def test_RZ_skipped(self, state_vector, n_wires): + """Tests whether the cascade of RZ gates is skipped for real-valued states""" + + n_CNOT = 2 ** n_wires - 2 + + dev = qml.device("default.qubit", wires=n_wires) + + @qml.qnode(dev) + def circuit(state_vector): + MottonenStatePreparation(state_vector, wires=range(n_wires)) + return qml.expval(qml.PauliX(wires=0)) + + # when the RZ cascade is skipped, CNOT gates should only be those required for RY cascade + circuit(state_vector) + + assert circuit.qtape.get_resources()["CNOT"] == n_CNOT + + @pytest.mark.parametrize( + "state_vector", [np.array([0.70710678, 0.70710678]), np.array([0.70710678, 0.70710678j])] + ) + def test_gradient_evaluated(self, state_vector): + """Test that the gradient is successfully calculated for a simple example. This test only + checks that the gradient is calculated without an error.""" + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def circuit(state_vector): + MottonenStatePreparation(state_vector, wires=range(1)) + return qml.expval(qml.PauliZ(0)) + + qml.grad(circuit)(state_vector) + class TestArbitraryStatePreparation: """Test the ArbitraryStatePreparation template.""" @@ -421,7 +499,7 @@ def test_correct_gates_two_wires(self): def test_GHZ_generation(self, qubit_device_3_wires, tol): """Test that the template prepares a GHZ state.""" - GHZ_state = np.array([1/math.sqrt(2), 0, 0, 0, 0, 0, 0, 1/math.sqrt(2)]) + GHZ_state = np.array([1 / math.sqrt(2), 0, 0, 0, 0, 0, 0, 1 / math.sqrt(2)]) weights = np.zeros(14) weights[13] = math.pi / 2 @@ -438,7 +516,7 @@ def circuit(weights): def test_even_superposition_generation(self, qubit_device_3_wires, tol): """Test that the template prepares a even superposition state.""" - even_superposition_state = np.ones(8)/math.sqrt(8) + even_superposition_state = np.ones(8) / math.sqrt(8) weights = np.zeros(14) weights[1] = math.pi / 2 diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 693ba96c60c..ef3f4a8e9a7 100644 --- a/tests/templates/test_subroutines.py +++ b/tests/templates/test_subroutines.py @@ -1487,8 +1487,8 @@ def test_expected_tape(self): m = qml.RX(0.3, wires=0).matrix - with qml.tape.QuantumTape() as tape: - QuantumPhaseEstimation(m, target_wires=[0], estimation_wires=[1, 2]) + op = QuantumPhaseEstimation(m, target_wires=[0], estimation_wires=[1, 2]) + tape = op.expand() with qml.tape.QuantumTape() as tape2: qml.Hadamard(1), @@ -1524,6 +1524,7 @@ def test_phase_estimated(self, phase): ) qml.probs(estimation_wires) + tape = tape.expand() res = tape.execute(dev).flatten() initial_estimate = np.argmax(res) / 2 ** (wires - 1) @@ -1571,6 +1572,7 @@ def test_phase_estimated_two_qubit(self): ) qml.probs(estimation_wires) + tape = tape.expand() res = tape.execute(dev).flatten() if phase < 0: diff --git a/tests/templates/test_subroutines_qmc.py b/tests/templates/test_subroutines_qmc.py index a64b111bdf3..f19ee38af4f 100644 --- a/tests/templates/test_subroutines_qmc.py +++ b/tests/templates/test_subroutines_qmc.py @@ -240,11 +240,11 @@ def test_expected_circuit(self): p = np.ones(4) / 4 target_wires, estimation_wires = Wires(range(3)), Wires(range(3, 5)) - with qml.tape.QuantumTape() as tape: - QuantumMonteCarlo(p, self.func, target_wires, estimation_wires) + op = QuantumMonteCarlo(p, self.func, target_wires, estimation_wires) + tape = op.expand().expand() - queue_before_qpe = tape.queue[:2] - queue_after_qpe = tape.queue[2:] + queue_before_qpe = tape.operations[:2] + queue_after_qpe = tape.operations[2:] A = probs_to_unitary(p) R = func_to_unitary(self.func, 4) @@ -262,12 +262,14 @@ def test_expected_circuit(self): with qml.tape.QuantumTape() as qpe_tape: qml.templates.QuantumPhaseEstimation(Q, target_wires, estimation_wires) - assert len(queue_after_qpe) == len(qpe_tape.queue) - assert all(o1.name == o2.name for o1, o2 in zip(queue_after_qpe, qpe_tape.queue)) + qpe_tape = qpe_tape.expand() + + assert len(queue_after_qpe) == len(qpe_tape.operations) + assert all(o1.name == o2.name for o1, o2 in zip(queue_after_qpe, qpe_tape.operations)) assert all( - np.allclose(o1.matrix, o2.matrix) for o1, o2 in zip(queue_after_qpe, qpe_tape.queue) + np.allclose(o1.matrix, o2.matrix) for o1, o2 in zip(queue_after_qpe, qpe_tape.operations) ) - assert all(o1.wires == o2.wires for o1, o2 in zip(queue_after_qpe, qpe_tape.queue)) + assert all(o1.wires == o2.wires for o1, o2 in zip(queue_after_qpe, qpe_tape.operations)) def test_expected_value(self): """Test that the QuantumMonteCarlo template can correctly estimate the expectation value diff --git a/tests/transforms/test_control.py b/tests/transforms/test_control.py new file mode 100644 index 00000000000..91608f72abe --- /dev/null +++ b/tests/transforms/test_control.py @@ -0,0 +1,212 @@ +from functools import partial +import numpy as np +import pennylane as qml +from pennylane.tape import QuantumTape +from pennylane.transforms.control import ctrl, ControlledOperation +from pennylane.tape.tape import expand_tape + + +def assert_equal_operations(ops1, ops2): + """Assert that two list of operations are equivalent""" + assert len(ops1) == len(ops2) + for op1, op2 in zip(ops1, ops2): + assert type(op1) == type(op2) + assert op1.wires == op2.wires + np.testing.assert_allclose(op1.parameters, op2.parameters) + + +def test_control_sanity_check(): + """Test that control works on a very standard usecase.""" + def make_ops(): + qml.RX(0.123, wires=0) + qml.RY(0.456, wires=2) + qml.RX(0.789, wires=0) + qml.Rot(0.111, 0.222, 0.333, wires=2), + qml.PauliX(wires=2) + qml.PauliY(wires=4) + qml.PauliZ(wires=0) + + with QuantumTape() as tape: + cmake_ops = ctrl(make_ops, control=1) + #Execute controlled version. + cmake_ops() + + expected = [ + qml.CRX(0.123, wires=[1, 0]), + qml.CRY(0.456, wires=[1, 2]), + qml.CRX(0.789, wires=[1, 0]), + qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]), + qml.CNOT(wires=[1, 2]), + qml.CY(wires=[1, 4]), + qml.CZ(wires=[1, 0]), + ] + assert len(tape.operations) == 1 + ctrl_op = tape.operations[0] + assert isinstance(ctrl_op, ControlledOperation) + expanded = ctrl_op.expand() + assert_equal_operations(expanded.operations, expected) + + +def test_adjoint_of_control(): + """Test adjoint(ctrl(fn)) and ctrl(adjoint(fn))""" + def my_op(a, b, c): + qml.RX(a, wires=2) + qml.RY(b, wires=3) + qml.RZ(c, wires=0) + + with QuantumTape() as tape1: + cmy_op_dagger = qml.adjoint(ctrl(my_op, 5)) + # Execute controlled and adjointed version of my_op. + cmy_op_dagger(0.789, 0.123, c=0.456) + + with QuantumTape() as tape2: + cmy_op_dagger = ctrl(qml.adjoint(my_op), 5) + # Execute adjointed and controlled version of my_op. + cmy_op_dagger(0.789, 0.123, c=0.456) + + expected = [ + qml.CRZ(-0.456, wires=[5, 0]), + qml.CRY(-0.123, wires=[5, 3]), + qml.CRX(-0.789, wires=[5, 2]), + ] + for tape in [tape1, tape2]: + assert len(tape.operations) == 1 + ctrl_op = tape.operations[0] + assert isinstance(ctrl_op, ControlledOperation) + expanded = ctrl_op.expand() + assert_equal_operations(expanded.operations, expected) + +def test_nested_control(): + """Test nested use of control""" + with QuantumTape() as tape: + CCX = ctrl(ctrl(qml.PauliX, 7), 3) + CCX(wires=0) + assert len(tape.operations) == 1 + op = tape.operations[0] + assert isinstance(op, ControlledOperation) + new_tape = expand_tape(tape, 3) + assert_equal_operations(new_tape.operations, [qml.Toffoli(wires=[7, 3, 0])]) + +def test_multi_control(): + """Test control with a list of wires.""" + with QuantumTape() as tape: + CCX = ctrl(qml.PauliX, control=[3, 7]) + CCX(wires=0) + assert len(tape.operations) == 1 + op = tape.operations[0] + assert isinstance(op, ControlledOperation) + new_tape = expand_tape(tape, 3) + assert_equal_operations(new_tape.operations, [qml.Toffoli(wires=[7, 3, 0])]) + +def test_control_with_qnode(): + """Test ctrl works when in a qnode cotext.""" + dev = qml.device("default.qubit", wires=3) + + def my_ansatz(params): + qml.RY(params[0], wires=0) + qml.RY(params[1], wires=1) + qml.CNOT(wires=[0, 1]) + qml.RX(params[2], wires=1) + qml.RX(params[3], wires=0) + qml.CNOT(wires=[1, 0]) + + def controlled_ansatz(params): + qml.CRY(params[0], wires=[2, 0]) + qml.CRY(params[1], wires=[2, 1]) + qml.Toffoli(wires=[2, 0, 1]) + qml.CRX(params[2], wires=[2, 1]) + qml.CRX(params[3], wires=[2, 0]) + qml.Toffoli(wires=[2, 1, 0]) + + def circuit(ansatz, params): + qml.RX(np.pi/4.0, wires=2) + ansatz(params) + return qml.state() + + params = [0.123, 0.456, 0.789, 1.345] + circuit1 = qml.qnode(dev)(partial(circuit, ansatz=ctrl(my_ansatz, 2))) + circuit2 = qml.qnode(dev)(partial(circuit, ansatz=controlled_ansatz)) + res1 = circuit1(params=params) + res2 = circuit2(params=params) + np.testing.assert_allclose(res1, res2) + + +def test_ctrl_within_ctrl(): + """Test using ctrl on a method that uses ctrl.""" + def ansatz(params): + qml.RX(params[0], wires=0) + ctrl(qml.PauliX, control=0)(wires=1) + qml.RX(params[1], wires=0) + + controlled_ansatz = ctrl(ansatz, 2) + + with QuantumTape() as tape: + controlled_ansatz([0.123, 0.456]) + + tape = expand_tape(tape, 2, stop_at=lambda op: not isinstance(op, ControlledOperation)) + + expected = [ + qml.CRX(0.123, wires=[2, 0]), + qml.Toffoli(wires=[0, 2, 1]), + qml.CRX(0.456, wires=[2, 0]) + ] + assert_equal_operations(tape.operations, expected) + +def test_diagonal_ctrl(): + """Test ctrl on diagonal gates.""" + with QuantumTape() as tape: + ctrl(qml.DiagonalQubitUnitary, 1)(np.array([-1.0, 1.0j]), wires=0) + tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation)) + assert_equal_operations( + tape.operations, + [ + qml.DiagonalQubitUnitary( + np.array([1.0, 1.0, -1.0, 1.0j]), + wires=[1, 0]) + ]) + +def test_qubit_unitary(): + """Test ctrl on QubitUnitary and ControlledQubitUnitary""" + with QuantumTape() as tape: + ctrl(qml.QubitUnitary, 1)( + np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0), + wires=0) + + tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation)) + assert_equal_operations( + tape.operations, + [ + qml.ControlledQubitUnitary( + np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0), + control_wires=1, + wires=0) + ]) + + with QuantumTape() as tape: + ctrl(qml.ControlledQubitUnitary, 1)( + np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0), + control_wires=2, + wires=0) + + tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation)) + assert_equal_operations( + tape.operations, + [ + qml.ControlledQubitUnitary( + np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0), + control_wires=[1, 2], + wires=0) + ]) + + +def test_no_control_defined(): + """Test a custom operation with no control transform defined.""" + # QFT has no control rule defined. + with QuantumTape() as tape: + ctrl(qml.QFT, 2)(wires=[0, 1]) + tape = expand_tape(tape) + assert len(tape.operations) == 12 + # Check that all operations are updated to their controlled version. + for op in tape.operations: + assert type(op) in {qml.ControlledPhaseShift, qml.Toffoli, qml.CRX, qml.CSWAP} +