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}
+