diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index e28ba584e4b..36a114468e0 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,6 +4,27 @@
New features since last release
+* Many parametrized operations now allow arguments with a batch dimension
+ [(#2535)](https://github.com/PennyLaneAI/pennylane/pull/2535)
+
+ This feature is not usable as a stand-alone but a technical requirement
+ for future performance improvements.
+ Previously unsupported batched parameters are allowed for example in
+ standard rotation gates. The batch dimension is the last dimension
+ of operator matrices, eigenvalues etc. Note that the batched parameter
+ has to be passed as an `array` but not as a python `list` or `tuple`.
+
+ ```pycon
+ >>> op = qml.RX(np.array([0.1, 0.2, 0.3], requires_grad=True), 0)
+ >>> np.round(op.matrix(), 4)
+ tensor([[[0.9988+0.j , 0.995 +0.j , 0.9888+0.j ],
+ [0. -0.05j , 0. -0.0998j, 0. -0.1494j]],
+
+ [[0. -0.05j , 0. -0.0998j, 0. -0.1494j],
+ [0.9988+0.j , 0.995 +0.j , 0.9888+0.j ]]], requires_grad=True)
+ >>> op.matrix().shape
+ (2, 2, 3)
+
* Boolean mask indexing of the parameter-shift Hessian
[(#2538)](https://github.com/PennyLaneAI/pennylane/pull/2538)
diff --git a/pennylane/operation.py b/pennylane/operation.py
index ae9c4a3dae3..4078203dec6 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -190,29 +190,39 @@ def expand_matrix(base_matrix, wires, wire_order):
# TODO[Maria]: In future we should consider making ``utils.expand`` differentiable and calling it here.
wire_order = Wires(wire_order)
n = len(wires)
- interface = qml.math._multi_dispatch(base_matrix) # pylint: disable=protected-access
+ shape = qml.math.shape(base_matrix)
+ batch_dim = shape[-1] if len(shape) == 3 else None
+ interface = qml.math.get_interface(base_matrix) # pylint: disable=protected-access
# operator's wire positions relative to wire ordering
op_wire_pos = wire_order.indices(wires)
identity = qml.math.reshape(
- qml.math.eye(2 ** len(wire_order), like=interface), [2] * len(wire_order) * 2
+ qml.math.eye(2 ** len(wire_order), like=interface), [2] * (len(wire_order) * 2)
)
axes = (list(range(n, 2 * n)), op_wire_pos)
# reshape op.matrix()
op_matrix_interface = qml.math.convert_like(base_matrix, identity)
- mat_op_reshaped = qml.math.reshape(op_matrix_interface, [2] * n * 2)
+ shape = [2] * (n * 2) + [batch_dim] if batch_dim else [2] * (n * 2)
+ mat_op_reshaped = qml.math.reshape(op_matrix_interface, shape)
mat_tensordot = qml.math.tensordot(
mat_op_reshaped, qml.math.cast_like(identity, mat_op_reshaped), axes
)
+ if batch_dim:
+ mat_tensordot = qml.math.moveaxis(mat_tensordot, n, -1)
unused_idxs = [idx for idx in range(len(wire_order)) if idx not in op_wire_pos]
# permute matrix axes to match wire ordering
perm = op_wire_pos + unused_idxs
- mat = qml.math.moveaxis(mat_tensordot, wire_order.indices(wire_order), perm)
+ sources = wire_order.indices(wire_order)
+ if batch_dim:
+ perm = perm + [-1]
+ sources = sources + [-1]
- mat = qml.math.reshape(mat, (2 ** len(wire_order), 2 ** len(wire_order)))
+ mat = qml.math.moveaxis(mat_tensordot, sources, perm)
+ shape = [2 ** len(wire_order)] * 2 + [batch_dim] if batch_dim else [2 ** len(wire_order)] * 2
+ mat = qml.math.reshape(mat, shape)
return mat
@@ -688,7 +698,14 @@ def eigvals(self):
# By default, compute the eigenvalues from the matrix representation.
# This will raise a NotImplementedError if the matrix is undefined.
try:
- return qml.math.linalg.eigvals(self.matrix())
+ mat = self.matrix()
+ if len(qml.math.shape(mat)) == 3:
+ # linalg.eigvals expects the last two dimensions to be the square dimension
+ # so that we have to transpose before and after the calculation.
+ return qml.math.transpose(
+ qml.math.linalg.eigvals(qml.math.transpose(mat, (2, 0, 1))), (1, 0)
+ )
+ return qml.math.linalg.eigvals(mat)
except MatrixUndefinedError as e:
raise EigvalsUndefinedError from e
@@ -804,7 +821,9 @@ def label(self, decimals=None, base_label=None, cache=None):
if len(qml.math.shape(params[0])) != 0:
# assume that if the first parameter is matrix-valued, there is only a single parameter
- # this holds true for all current operations and templates
+ # this holds true for all current operations and templates unless tensor-batching
+ # is used
+ # TODO[dwierichs]: Implement a proper label for tensor-batched operators
if (
cache is None
or not isinstance(cache.get("matrices", None), list)
@@ -1404,7 +1423,7 @@ def matrix(self, wire_order=None):
canonical_matrix = self.compute_matrix(*self.parameters, **self.hyperparameters)
if self.inverse:
- canonical_matrix = qml.math.conj(qml.math.T(canonical_matrix))
+ canonical_matrix = qml.math.conj(qml.math.moveaxis(canonical_matrix, 0, 1))
if wire_order is None or self.wires == Wires(wire_order):
return canonical_matrix
diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py
index 4924c7b0839..d9d6a1c21e8 100644
--- a/pennylane/ops/functions/matrix.py
+++ b/pennylane/ops/functions/matrix.py
@@ -141,6 +141,7 @@ def _matrix(tape, wire_order=None):
for op in tape.operations:
U = matrix(op, wire_order=wire_order)
- unitary_matrix = qml.math.dot(U, unitary_matrix)
+ unitary_matrix = qml.math.tensordot(U, unitary_matrix, axes=[[1], [0]])
+ unitary_matrix = qml.math.moveaxis(unitary_matrix, 1, -1)
return unitary_matrix
diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py
index 62ef3c86cad..24f67b57b78 100644
--- a/pennylane/ops/qubit/attributes.py
+++ b/pennylane/ops/qubit/attributes.py
@@ -199,3 +199,28 @@ def __contains__(self, obj):
representation using ``np.linalg.eigvals``, which fails for some tensor types that the matrix
may be cast in on backpropagation devices.
"""
+
+supports_tensorbatching = Attribute(
+ [
+ "QubitUnitary",
+ "DiagonalQubitUnitary",
+ "RX",
+ "RY",
+ "RZ",
+ "PhaseShift",
+ "ControlledPhaseShift",
+ "Rot",
+ "MultiRZ",
+ "PauliRot",
+ "CRX",
+ "CRY",
+ "CRZ",
+ "CRot",
+ "U1",
+ "U2",
+ "U3",
+ "IsingXX",
+ "IsingYY",
+ "IsingZZ",
+ ]
+)
diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py
index b67ed245541..c5434e8382f 100644
--- a/pennylane/ops/qubit/matrix_ops.py
+++ b/pennylane/ops/qubit/matrix_ops.py
@@ -65,20 +65,27 @@ def __init__(self, *params, wires, do_queue=True):
# of wires fits the dimensions of the matrix
if not isinstance(self, ControlledQubitUnitary):
U = params[0]
+ U_shape = qml.math.shape(U)
dim = 2 ** len(wires)
- if qml.math.shape(U) != (dim, dim):
+ if not (len(U_shape) in {2, 3} and U_shape[:2] == (dim, dim)):
raise ValueError(
- f"Input unitary must be of shape {(dim, dim)} to act on {len(wires)} wires."
+ f"Input unitary must be of shape {(dim, dim)} or ({dim, dim}, batch_dim) "
+ f"to act on {len(wires)} wires."
)
# Check for unitarity; due to variable precision across the different ML frameworks,
# here we issue a warning to check the operation, instead of raising an error outright.
- if not qml.math.is_abstract(U) and not qml.math.allclose(
- qml.math.dot(U, qml.math.T(qml.math.conj(U))),
- qml.math.eye(qml.math.shape(U)[0]),
- atol=1e-6,
+ # TODO[dwierichs]: Implement unitarity check also for tensor-batched arguments U
+ if not (
+ qml.math.is_abstract(U)
+ or len(U_shape) == 3
+ or qml.math.allclose(
+ qml.math.dot(U, qml.math.T(qml.math.conj(U))),
+ qml.math.eye(dim),
+ atol=1e-6,
+ )
):
warnings.warn(
f"Operator {U}\n may not be unitary."
@@ -142,16 +149,25 @@ def compute_decomposition(U, wires):
"""
# Decomposes arbitrary single-qubit unitaries as Rot gates (RZ - RY - RZ format),
# or a single RZ for diagonal matrices.
- if qml.math.shape(U) == (2, 2):
+ shape = qml.math.shape(U)
+ if shape == (2, 2):
return qml.transforms.decompositions.zyz_decomposition(U, Wires(wires)[0])
- if qml.math.shape(U) == (4, 4):
+ if shape == (4, 4):
return qml.transforms.two_qubit_decomposition(U, Wires(wires))
+ # TODO[dwierichs]: Implement decomposition of tensor-batched unitary
+ if len(shape) == 3:
+ raise DecompositionUndefinedError(
+ "The decomposition of QubitUnitary does not support tensor-batching."
+ )
+
return super(QubitUnitary, QubitUnitary).compute_decomposition(U, wires=wires)
def adjoint(self):
- return QubitUnitary(qml.math.T(qml.math.conj(self.matrix())), wires=self.wires)
+ U = self.matrix()
+ axis = (1, 0) if len(qml.math.shape(U)) == 2 else (1, 0, 2)
+ return QubitUnitary(qml.math.transpose(qml.math.conj(U), axis), wires=self.wires)
def pow(self, z):
if isinstance(z, int):
@@ -237,6 +253,10 @@ def __init__(
"The control wires must be different from the wires specified to apply the unitary on."
)
+ # TODO[dwierichs]: Implement tensor-batching
+ if len(qml.math.shape(params[0])) == 3:
+ raise NotImplementedError("ControlledQubitUnitary does not support tensor-batching.")
+
self._hyperparameters = {
"u_wires": wires,
"control_wires": control_wires,
@@ -389,6 +409,11 @@ def compute_matrix(D): # pylint: disable=arguments-differ
if not qml.math.allclose(D * qml.math.conj(D), qml.math.ones_like(D)):
raise ValueError("Operator must be unitary.")
+ if len(qml.math.shape(D)) == 2:
+ return qml.math.transpose(
+ qml.math.stack([qml.math.diag(_D) for _D in qml.math.T(D)]), (1, 2, 0)
+ )
+
return qml.math.diag(D)
@staticmethod
@@ -419,8 +444,9 @@ def compute_eigvals(D): # pylint: disable=arguments-differ
"""
D = qml.math.asarray(D)
- if not qml.math.is_abstract(D) and not qml.math.allclose(
- D * qml.math.conj(D), qml.math.ones_like(D)
+ if not (
+ qml.math.is_abstract(D)
+ or qml.math.allclose(D * qml.math.conj(D), qml.math.ones_like(D))
):
raise ValueError("Operator must be unitary.")
@@ -450,7 +476,7 @@ def compute_decomposition(D, wires):
[QubitUnitary(array([[1, 0], [0, 1]]), wires=[0])]
"""
- return [QubitUnitary(qml.math.diag(D), wires=wires)]
+ return [QubitUnitary(DiagonalQubitUnitary.compute_matrix(D), wires=wires)]
def adjoint(self):
return DiagonalQubitUnitary(qml.math.conj(self.parameters[0]), wires=self.wires)
diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py
index 09c39fbd29b..77cfeed32ef 100644
--- a/pennylane/ops/qubit/parametric_ops.py
+++ b/pennylane/ops/qubit/parametric_ops.py
@@ -26,7 +26,8 @@
import pennylane as qml
from pennylane.operation import AnyWires, Operation
from pennylane.ops.qubit.non_parametric_ops import PauliX, PauliY, PauliZ, Hadamard
-from pennylane.utils import expand, pauli_eigs
+from pennylane.operation import expand_matrix
+from pennylane.utils import pauli_eigs
from pennylane.wires import Wires
INV_SQRT2 = 1 / math.sqrt(2)
@@ -97,11 +98,10 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
c = qml.math.cast_like(c, 1j)
s = qml.math.cast_like(s, 1j)
+ c = (1 + 0j) * c
js = -1j * s
-
- return qml.math.diag([c, c]) + qml.math.stack(
- [qml.math.stack([0, js]), qml.math.stack([js, 0])]
- )
+ return qml.math.stack([qml.math.stack([c, js]), qml.math.stack([js, c])])
+ # return mat * qml.math.array([[1+0j, -1j], [-1j, 1+0j]], like=mat)
def adjoint(self):
return RX(-self.data[0], wires=self.wires)
@@ -114,7 +114,8 @@ def _controlled(self, wire):
def single_qubit_rot_angles(self):
# RX(\theta) = RZ(-\pi/2) RY(\theta) RZ(\pi/2)
- return [np.pi / 2, self.data[0], -np.pi / 2]
+ pi_half = qml.math.ones_like(self.data[0]) * (np.pi / 2)
+ return [pi_half, self.data[0], -pi_half]
class RY(Operation):
@@ -179,10 +180,12 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
c = qml.math.cos(theta / 2)
s = qml.math.sin(theta / 2)
-
- return qml.math.diag([c, c]) + qml.math.stack(
- [qml.math.stack([0, -s]), qml.math.stack([s, 0])]
- )
+ if qml.math.get_interface(theta) == "tensorflow":
+ c = qml.math.cast_like(c, 1j)
+ s = qml.math.cast_like(s, 1j)
+ c = (1 + 0j) * c
+ s = (1 + 0j) * s
+ return qml.math.stack([qml.math.stack([c, -s]), qml.math.stack([s, c])])
def adjoint(self):
return RY(-self.data[0], wires=self.wires)
@@ -260,8 +263,9 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
theta = qml.math.cast_like(theta, 1j)
p = qml.math.exp(-0.5j * theta)
+ z = qml.math.zeros_like(p)
- return qml.math.diag([p, qml.math.conj(p)])
+ return qml.math.stack([qml.math.stack([p, z]), qml.math.stack([z, qml.math.conj(p)])])
@staticmethod
def compute_eigvals(theta): # pylint: disable=arguments-differ
@@ -377,9 +381,10 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
if qml.math.get_interface(phi) == "tensorflow":
phi = qml.math.cast_like(phi, 1j)
- exp_part = qml.math.exp(1j * phi)
+ p = qml.math.exp(1j * phi)
+ z = qml.math.zeros_like(p)
- return qml.math.diag([1, exp_part])
+ return qml.math.stack([qml.math.stack([qml.math.ones_like(p), z]), qml.math.stack([z, p])])
@staticmethod
def compute_eigvals(phi): # pylint: disable=arguments-differ
@@ -411,9 +416,9 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ
if qml.math.get_interface(phi) == "tensorflow":
phi = qml.math.cast_like(phi, 1j)
- exp_part = qml.math.exp(1j * phi)
+ p = qml.math.exp(1j * phi)
- return qml.math.stack([1, exp_part])
+ return qml.math.stack([qml.math.ones_like(p), p])
@staticmethod
def compute_decomposition(phi, wires):
@@ -524,6 +529,18 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
phi = qml.math.cast_like(phi, 1j)
exp_part = qml.math.exp(1j * phi)
+ shape = qml.math.shape(phi)
+ if len(shape) > 0:
+ ones = qml.math.ones_like(exp_part)
+ zeros = qml.math.zeros_like(exp_part)
+ matrix = [
+ [ones, zeros, zeros, zeros],
+ [zeros, ones, zeros, zeros],
+ [zeros, zeros, ones, zeros],
+ [zeros, zeros, zeros, exp_part],
+ ]
+
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
return qml.math.diag([1, 1, 1, exp_part])
@@ -558,8 +575,8 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ
phi = qml.math.cast_like(phi, 1j)
exp_part = qml.math.exp(1j * phi)
-
- return qml.math.stack([1, 1, 1, exp_part])
+ ones = qml.math.ones_like(exp_part)
+ return qml.math.stack([ones, ones, ones, exp_part])
@staticmethod
def compute_decomposition(phi, wires):
@@ -818,7 +835,15 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ
theta = qml.math.cast_like(theta, 1j)
eigs = qml.math.cast_like(eigs, 1j)
- eigvals = qml.math.exp(-1j * theta / 2 * eigs)
+ shape = qml.math.shape(theta)
+ if len(shape) > 0:
+ eigvals = qml.math.exp(qml.math.tensordot(eigs, -0.5j * theta, axes=0))
+ dim = 2**num_wires
+ mat = qml.math.zeros(((dim, dim) + shape), like=eigvals, dtype=complex)
+ mat[np.diag_indices(dim, ndim=2)] = eigvals
+ return mat
+
+ eigvals = qml.math.exp(-0.5j * theta * eigs)
return qml.math.diag(eigvals)
def generator(self):
@@ -859,7 +884,10 @@ def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ
theta = qml.math.cast_like(theta, 1j)
eigs = qml.math.cast_like(eigs, 1j)
- return qml.math.exp(-1j * theta / 2 * eigs)
+ if len(qml.math.shape(theta)) > 0:
+ return qml.math.exp(qml.math.tensordot(eigs, -0.5j * theta, axes=0))
+
+ return qml.math.exp(-0.5j * theta * eigs)
@staticmethod
def compute_decomposition(
@@ -1000,7 +1028,8 @@ def label(self, decimals=None, base_label=None, cache=None):
if self.inverse:
op_label += "⁻¹"
- if decimals is not None:
+ # TODO[dwierichs]: Implement a proper label for tensor-batched operators
+ if decimals is not None and qml.math.shape(self.parameters[0]) == ():
param_string = f"\n({qml.math.asarray(self.parameters[0]):.{decimals}f})"
op_label += param_string
@@ -1016,7 +1045,7 @@ def _check_pauli_word(pauli_word):
Returns:
bool: Whether the Pauli word has correct structure.
"""
- return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in pauli_word)
+ return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in set(pauli_word))
@staticmethod
def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ
@@ -1053,9 +1082,13 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ
theta = qml.math.cast_like(theta, 1j)
# Simplest case is if the Pauli is the identity matrix
- if pauli_word == "I" * len(pauli_word):
+ if set(pauli_word) == {"I"}:
- exp = qml.math.exp(-1j * theta / 2)
+ if len(qml.math.shape(theta)) > 0:
+ raise NotImplementedError(
+ "PauliRot with the identity matrix does not support tensor-batching."
+ )
+ exp = qml.math.exp(-0.5j * theta)
iden = qml.math.eye(2 ** len(pauli_word))
if interface == "torch":
# Use convert_like to ensure that the tensor is put on the correct
@@ -1078,11 +1111,15 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ
qml.math.kron,
[PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates],
)
-
- return expand(
- qml.math.dot(
+ if interface == "tensorflow":
+ conjugation_matrix = qml.math.cast_like(conjugation_matrix, 1j)
+ # Note: we use einsum with reverse arguments here because it is not multi-dispatched
+ # and the tensordot containing multi_Z_rot_matrix should decide about the interface
+ return expand_matrix(
+ qml.math.einsum(
+ "jk...,ij->i...k",
+ qml.math.tensordot(multi_Z_rot_matrix, conjugation_matrix, axes=[[1], [0]]),
qml.math.conj(conjugation_matrix),
- qml.math.dot(multi_Z_rot_matrix, conjugation_matrix),
),
non_identity_wires,
list(range(len(pauli_word))),
@@ -1121,8 +1158,12 @@ def compute_eigvals(theta, pauli_word): # pylint: disable=arguments-differ
theta = qml.math.cast_like(theta, 1j)
# Identity must be treated specially because its eigenvalues are all the same
- if pauli_word == "I" * len(pauli_word):
- return qml.math.exp(-1j * theta / 2) * qml.math.ones(2 ** len(pauli_word))
+ if set(pauli_word) == {"I"}:
+ if len(qml.math.shape(theta)) > 0:
+ raise NotImplementedError(
+ "PauliRot with the identity matrix does not support tensor-batching."
+ )
+ return qml.math.exp(-0.5j * theta) * qml.math.ones(2 ** len(pauli_word))
return MultiRZ.compute_eigvals(theta, len(pauli_word))
@@ -1272,24 +1313,22 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
c = qml.math.cos(theta / 2)
s = qml.math.sin(theta / 2)
- if interface == "torch":
- # Use convert_like to ensure that the tensor is put on the correct
- # Torch device
- z = qml.math.convert_like(qml.math.zeros([4]), theta)
- else:
- z = qml.math.zeros([4], like=interface)
-
if interface == "tensorflow":
c = qml.math.cast_like(c, 1j)
s = qml.math.cast_like(s, 1j)
- z = qml.math.cast_like(z, 1j)
+ c = (1 + 0j) * c
js = -1j * s
+ ones = qml.math.ones_like(js)
+ zeros = qml.math.zeros_like(js)
+ matrix = [
+ [ones, zeros, zeros, zeros],
+ [zeros, ones, zeros, zeros],
+ [zeros, zeros, c, js],
+ [zeros, zeros, js, c],
+ ]
- mat = qml.math.diag([1, 1, c, c])
- return mat + qml.math.stack(
- [z, z, qml.math.stack([0, 0, 0, js]), qml.math.stack([0, 0, js, 0])]
- )
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
@staticmethod
def compute_decomposition(phi, wires):
@@ -1318,13 +1357,14 @@ def compute_decomposition(phi, wires):
RZ(-1.5707963267948966, wires=[1])]
"""
+ pi_half = qml.math.ones_like(phi) * (np.pi / 2)
decomp_ops = [
- RZ(np.pi / 2, wires=wires[1]),
+ RZ(pi_half, wires=wires[1]),
RY(phi / 2, wires=wires[1]),
qml.CNOT(wires=wires),
RY(-phi / 2, wires=wires[1]),
qml.CNOT(wires=wires),
- RZ(-np.pi / 2, wires=wires[1]),
+ RZ(-pi_half, wires=wires[1]),
]
return decomp_ops
@@ -1425,17 +1465,22 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
c = qml.math.cos(theta / 2)
s = qml.math.sin(theta / 2)
- if interface == "torch":
- # Use convert_like to ensure that the tensor is put on the correct
- # Torch device
- z = qml.math.convert_like(qml.math.zeros([4]), theta)
- else:
- z = qml.math.zeros([4], like=interface)
+ if interface == "tensorflow":
+ c = qml.math.cast_like(c, 1j)
+ s = qml.math.cast_like(s, 1j)
+
+ c = (1 + 0j) * c
+ s = (1 + 0j) * s
+ ones = qml.math.ones_like(s)
+ zeros = qml.math.zeros_like(s)
+ matrix = [
+ [ones, zeros, zeros, zeros],
+ [zeros, ones, zeros, zeros],
+ [zeros, zeros, c, -s],
+ [zeros, zeros, s, c],
+ ]
- mat = qml.math.diag([1, 1, c, c])
- return mat + qml.math.stack(
- [z, z, qml.math.stack([0, 0, 0, -s]), qml.math.stack([0, 0, s, 0])]
- )
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
@staticmethod
def compute_decomposition(phi, wires):
@@ -1567,9 +1612,18 @@ def compute_matrix(theta): # pylint: disable=arguments-differ
if qml.math.get_interface(theta) == "tensorflow":
theta = qml.math.cast_like(theta, 1j)
- exp_part = qml.math.exp(-0.5j * theta)
+ exp_part = qml.math.exp(-1j * theta / 2)
+
+ ones = qml.math.ones_like(exp_part)
+ zeros = qml.math.zeros_like(exp_part)
+ matrix = [
+ [ones, zeros, zeros, zeros],
+ [zeros, ones, zeros, zeros],
+ [zeros, zeros, exp_part, zeros],
+ [zeros, zeros, zeros, qml.math.conj(exp_part)],
+ ]
- return qml.math.diag([1, 1, exp_part, qml.math.conj(exp_part)])
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
@staticmethod
def compute_eigvals(theta): # pylint: disable=arguments-differ
@@ -1598,14 +1652,15 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ
>>> qml.CRZ.compute_eigvals(torch.tensor(0.5))
tensor([1.0000+0.0000j, 1.0000+0.0000j, 0.9689-0.2474j, 0.9689+0.2474j])
"""
- theta = qml.math.flatten(qml.math.stack([theta]))[0]
+ # theta = qml.math.flatten(qml.math.stack([theta]))[0]
if qml.math.get_interface(theta) == "tensorflow":
theta = qml.math.cast_like(theta, 1j)
exp_part = qml.math.exp(-0.5j * theta)
+ o = qml.math.ones_like(exp_part)
- return qml.math.stack([1, 1, exp_part, qml.math.conj(exp_part)])
+ return qml.math.stack([o, o, exp_part, qml.math.conj(exp_part)])
@staticmethod
def compute_decomposition(phi, wires):
@@ -1745,18 +1800,20 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ
c = qml.math.cast_like(qml.math.asarray(c, like=interface), 1j)
s = qml.math.cast_like(qml.math.asarray(s, like=interface), 1j)
+ o = qml.math.ones_like(c)
+ z = qml.math.zeros_like(c)
mat = [
- [1, 0, 0, 0],
- [0, 1, 0, 0],
+ [o, z, z, z],
+ [z, o, z, z],
[
- 0,
- 0,
+ z,
+ z,
qml.math.exp(-0.5j * (phi + omega)) * c,
-qml.math.exp(0.5j * (phi - omega)) * s,
],
[
- 0,
- 0,
+ z,
+ z,
qml.math.exp(-0.5j * (phi - omega)) * s,
qml.math.exp(0.5j * (phi + omega)) * c,
],
@@ -1878,9 +1935,10 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
if qml.math.get_interface(phi) == "tensorflow":
phi = qml.math.cast_like(phi, 1j)
- exp_part = qml.math.exp(1j * phi)
+ p = qml.math.exp(1j * phi)
+ z = qml.math.zeros_like(p)
- return qml.math.diag([1, exp_part])
+ return qml.math.stack([qml.math.stack([qml.math.ones_like(p), z]), qml.math.stack([z, p])])
@staticmethod
def compute_decomposition(phi, wires):
@@ -1990,7 +2048,7 @@ def compute_matrix(phi, delta): # pylint: disable=arguments-differ
delta = qml.math.cast_like(qml.math.asarray(delta, like=interface), 1j)
mat = [
- [1, -qml.math.exp(1j * delta)],
+ [qml.math.ones_like(phi), -qml.math.exp(1j * delta)],
[qml.math.exp(1j * phi), qml.math.exp(1j * (phi + delta))],
]
@@ -2020,8 +2078,9 @@ def compute_decomposition(phi, delta, wires):
PhaseShift(1.23, wires=[0])]
"""
+ pi_half = qml.math.ones_like(delta) * (np.pi / 2)
decomp_ops = [
- Rot(delta, np.pi / 2, -delta, wires=wires),
+ Rot(delta, pi_half, -delta, wires=wires),
PhaseShift(delta, wires=wires),
PhaseShift(phi, wires=wires),
]
@@ -2029,8 +2088,8 @@ def compute_decomposition(phi, delta, wires):
def adjoint(self):
phi, delta = self.parameters
- new_delta = (np.pi - phi) % (2 * np.pi)
- new_phi = (np.pi - delta) % (2 * np.pi)
+ new_delta = qml.math.mod((np.pi - phi), (2 * np.pi))
+ new_phi = qml.math.mod((np.pi - delta), (2 * np.pi))
return U2(new_phi, new_delta, wires=self.wires)
@@ -2163,8 +2222,8 @@ def compute_decomposition(theta, phi, delta, wires):
def adjoint(self):
theta, phi, delta = self.parameters
- new_delta = (np.pi - phi) % (2 * np.pi)
- new_phi = (np.pi - delta) % (2 * np.pi)
+ new_delta = qml.math.mod((np.pi - phi), (2 * np.pi))
+ new_phi = qml.math.mod((np.pi - delta), (2 * np.pi))
return U3(theta, new_phi, new_delta, wires=self.wires)
@@ -2232,15 +2291,22 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
"""
c = qml.math.cos(phi / 2)
s = qml.math.sin(phi / 2)
- Y = qml.math.convert_like(np.eye(4)[::-1].copy(), phi)
if qml.math.get_interface(phi) == "tensorflow":
c = qml.math.cast_like(c, 1j)
s = qml.math.cast_like(s, 1j)
- Y = qml.math.cast_like(Y, 1j)
- mat = qml.math.diag([c, c, c, c]) - 1j * s * Y
- return mat
+ c = (1 + 0j) * c
+ js = -1j * s
+ z = qml.math.zeros_like(js)
+
+ matrix = [
+ [c, z, z, js],
+ [z, c, js, z],
+ [z, js, c, z],
+ [js, z, z, c],
+ ]
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
@staticmethod
def compute_decomposition(phi, wires):
@@ -2371,14 +2437,22 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
"""
c = qml.math.cos(phi / 2)
s = qml.math.sin(phi / 2)
- Y = qml.math.convert_like(np.diag([1, -1, -1, 1])[::-1].copy(), phi)
if qml.math.get_interface(phi) == "tensorflow":
c = qml.math.cast_like(c, 1j)
s = qml.math.cast_like(s, 1j)
- Y = qml.math.cast_like(Y, 1j)
- return qml.math.diag([c, c, c, c]) + 1j * s * Y
+ c = (1 + 0j) * c
+ js = 1j * s
+ z = qml.math.zeros_like(js)
+
+ matrix = [
+ [c, z, z, js],
+ [z, c, -js, z],
+ [z, -js, c, z],
+ [js, z, z, c],
+ ]
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
def adjoint(self):
(phi,) = self.parameters
@@ -2481,10 +2555,18 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
if qml.math.get_interface(phi) == "tensorflow":
phi = qml.math.cast_like(phi, 1j)
- pos_phase = qml.math.exp(1.0j * phi / 2)
- neg_phase = qml.math.exp(-1.0j * phi / 2)
+ neg_phase = qml.math.exp(-0.5j * phi)
+ pos_phase = qml.math.exp(0.5j * phi)
+
+ zeros = qml.math.zeros_like(pos_phase)
+ matrix = [
+ [neg_phase, zeros, zeros, zeros],
+ [zeros, pos_phase, zeros, zeros],
+ [zeros, zeros, pos_phase, zeros],
+ [zeros, zeros, zeros, neg_phase],
+ ]
- return qml.math.diag([neg_phase, pos_phase, pos_phase, neg_phase])
+ return qml.math.stack([qml.math.stack(row) for row in matrix])
@staticmethod
def compute_eigvals(phi): # pylint: disable=arguments-differ
diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py
index 1a03f97dc83..4e466d81d10 100644
--- a/tests/ops/qubit/test_matrix_ops.py
+++ b/tests/ops/qubit/test_matrix_ops.py
@@ -20,6 +20,7 @@
import pennylane as qml
from pennylane.wires import Wires
+from pennylane.operation import DecompositionUndefinedError
from gate_data import (
I,
@@ -64,7 +65,9 @@ def test_qubit_unitary_pow(self, n):
assert qml.math.allclose(mat_to_pow, new_mat)
@pytest.mark.autograd
- @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)])
+ @pytest.mark.parametrize(
+ "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot(H, [1j, -1, 1], axes=0), 1)]
+ )
def test_qubit_unitary_autograd(self, U, num_wires):
"""Test that the unitary operator produces the correct output and
catches incorrect input with autograd."""
@@ -81,18 +84,21 @@ def test_qubit_unitary_autograd(self, U, num_wires):
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix()
- # test non-unitary matrix
- U3 = U.copy()
- U3[0, 0] += 0.5
- with pytest.warns(UserWarning, match="may not be unitary"):
- qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
+ if len(U.shape) == 2:
+ # test non-unitary matrix if it is not batched (not implemented yet for batching)
+ U3 = U.copy()
+ U3[0, 0] += 0.5
+ with pytest.warns(UserWarning, match="may not be unitary"):
+ qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
# test an error is thrown when constructed with incorrect number of wires
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix()
@pytest.mark.torch
- @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)])
+ @pytest.mark.parametrize(
+ "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot(H, [1j, -1, 1], axes=0), 1)]
+ )
def test_qubit_unitary_torch(self, U, num_wires):
"""Test that the unitary operator produces the correct output and
catches incorrect input with torch."""
@@ -111,18 +117,21 @@ def test_qubit_unitary_torch(self, U, num_wires):
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix()
- # test non-unitary matrix
- U3 = U.detach().clone()
- U3[0, 0] += 0.5
- with pytest.warns(UserWarning, match="may not be unitary"):
- qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
+ if len(U.shape) == 2:
+ # test non-unitary matrix if it is not batched (not implemented yet for batching)
+ U3 = U.detach().clone()
+ U3[0, 0] += 0.5
+ with pytest.warns(UserWarning, match="may not be unitary"):
+ qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
# test an error is thrown when constructed with incorrect number of wires
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix()
@pytest.mark.tf
- @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)])
+ @pytest.mark.parametrize(
+ "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot(H, [1j, -1, 1], axes=0), 1)]
+ )
def test_qubit_unitary_tf(self, U, num_wires):
"""Test that the unitary operator produces the correct output and
catches incorrect input with tensorflow."""
@@ -141,17 +150,20 @@ def test_qubit_unitary_tf(self, U, num_wires):
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix()
- # test non-unitary matrix
- U3 = tf.Variable(U + 0.5)
- with pytest.warns(UserWarning, match="may not be unitary"):
- qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
+ if len(U.shape) == 2:
+ # test non-unitary matrix if it is not batched (not implemented yet for batching)
+ U3 = tf.Variable(U + 0.5)
+ with pytest.warns(UserWarning, match="may not be unitary"):
+ qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
# test an error is thrown when constructed with incorrect number of wires
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix()
@pytest.mark.jax
- @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)])
+ @pytest.mark.parametrize(
+ "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot(H, [1j, -1, 1], axes=0), 1)]
+ )
def test_qubit_unitary_jax(self, U, num_wires):
"""Test that the unitary operator produces the correct output and
catches incorrect input with jax."""
@@ -170,18 +182,21 @@ def test_qubit_unitary_jax(self, U, num_wires):
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix()
- # test non-unitary matrix
- U3 = U + 0.5
- with pytest.warns(UserWarning, match="may not be unitary"):
- qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
+ if len(U.shape) == 2:
+ # test non-unitary matrix if it is not batched (not implemented yet for batching)
+ U3 = U + 0.5
+ with pytest.warns(UserWarning, match="may not be unitary"):
+ qml.QubitUnitary(U3, wires=range(num_wires)).matrix()
# test an error is thrown when constructed with incorrect number of wires
with pytest.raises(ValueError, match="must be of shape"):
qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix()
@pytest.mark.jax
- @pytest.mark.parametrize("U, num_wires", [(H, 1), (np.kron(H, H), 2)])
- def test_qubit_unitary_jax(self, U, num_wires):
+ @pytest.mark.parametrize(
+ "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot(H, [1j, -1, 1], axes=0), 1)]
+ )
+ def test_qubit_unitary_jax_jit(self, U, num_wires):
"""Tests that QubitUnitary works with jitting."""
import jax
from jax import numpy as jnp
@@ -231,6 +246,14 @@ def test_qubit_unitary_decomposition(self, U, expected_gate, expected_params):
assert isinstance(decomp2[0], expected_gate)
assert np.allclose(decomp2[0].parameters, expected_params, atol=1e-7)
+ def test_error_qubit_unitary_decomposition_batched(self):
+ """Tests that single-qubit QubitUnitary decompositions are performed."""
+ U = np.ones((2, 2, 3))
+ with pytest.raises(DecompositionUndefinedError, match="QubitUnitary does not support"):
+ qml.QubitUnitary.compute_decomposition(U, wires=0)
+ with pytest.raises(DecompositionUndefinedError, match="QubitUnitary does not support"):
+ qml.QubitUnitary(U, wires=0).decomposition()
+
def test_qubit_unitary_decomposition_multiqubit_invalid(self):
"""Test that QubitUnitary is not decomposed for more than two qubits."""
U = qml.Toffoli(wires=[0, 1, 2]).matrix()
@@ -249,21 +272,48 @@ def test_matrix_representation(self, tol):
assert np.allclose(res_static, expected, atol=tol)
assert np.allclose(res_dynamic, expected, atol=tol)
+ def test_matrix_representation_batched(self, tol):
+ """Test that the matrix representation is defined correctly"""
+ U = np.array(
+ [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]]
+ )
+ U = np.tensordot(U, [0.2, -0.1, 1.3], axes=0)
+ res_static = qml.QubitUnitary.compute_matrix(U)
+ res_dynamic = qml.QubitUnitary(U, wires=0).matrix()
+ expected = U
+ assert np.allclose(res_static, expected, atol=tol)
+ assert np.allclose(res_dynamic, expected, atol=tol)
+
class TestDiagonalQubitUnitary:
"""Test the DiagonalQubitUnitary operation."""
- def test_decomposition(self):
+ @pytest.mark.parametrize(
+ "D, num_wires, expected_U",
+ [
+ ([1j, 1, 1, -1, -1j, 1j, 1, -1], 3, np.diag([1j, 1, 1, -1, -1j, 1j, 1, -1])),
+ (
+ np.outer([1.0, -1.0, 1j, 1.0], [1.0, -1.0]),
+ 2,
+ np.transpose(
+ np.stack([np.diag([1.0, -1.0, 1j, 1.0]), np.diag([-1.0, 1.0, -1j, -1.0])]),
+ (1, 2, 0),
+ ),
+ ),
+ ],
+ )
+ def test_decomposition(self, D, num_wires, expected_U):
"""Test that DiagonalQubitUnitary falls back to QubitUnitary."""
- D = np.array([1j, 1, 1, -1, -1j, 1j, 1, -1])
+ D = np.array(D)
- decomp = qml.DiagonalQubitUnitary.compute_decomposition(D, [0, 1, 2])
- decomp2 = qml.DiagonalQubitUnitary(D, wires=[0, 1, 2]).decomposition()
+ wires = list(range(num_wires))
+ decomp = qml.DiagonalQubitUnitary.compute_decomposition(D, wires)
+ decomp2 = qml.DiagonalQubitUnitary(D, wires=wires).decomposition()
assert decomp[0].name == "QubitUnitary" == decomp2[0].name
- assert decomp[0].wires == Wires([0, 1, 2]) == decomp2[0].wires
- assert np.allclose(decomp[0].data[0], np.diag(D))
- assert np.allclose(decomp2[0].data[0], np.diag(D))
+ assert decomp[0].wires == Wires(wires) == decomp2[0].wires
+ assert np.allclose(decomp[0].data[0], expected_U)
+ assert np.allclose(decomp2[0].data[0], expected_U)
def test_controlled(self):
"""Test that the correct controlled operation is created when controlling a qml.DiagonalQubitUnitary."""
@@ -276,6 +326,30 @@ def test_controlled(self):
mat, qml.math.diag(qml.math.append(qml.math.ones(8, dtype=complex), D))
)
+ def test_controlled_batched(self):
+ """Test that the correct controlled operation is created when
+ controlling a qml.DiagonalQubitUnitary with a batched diagonal."""
+ D = np.array([[1j, 1], [1, -1], [-1j, 1j], [1, -1]])
+ op = qml.DiagonalQubitUnitary(D, wires=[1, 2])
+ with qml.tape.QuantumTape() as tape:
+ op._controlled(control=0)
+ mat = qml.matrix(tape)
+ z = [0, 0]
+ o = [1, 1]
+ expected = np.array(
+ [
+ [o, z, z, z, z, z, z, z],
+ [z, o, z, z, z, z, z, z],
+ [z, z, o, z, z, z, z, z],
+ [z, z, z, o, z, z, z, z],
+ [z, z, z, z, [1j, 1], z, z, z],
+ [z, z, z, z, z, [1, -1], z, z],
+ [z, z, z, z, z, z, [-1j, 1j], z],
+ [z, z, z, z, z, z, z, [1, -1]],
+ ]
+ )
+ assert qml.math.allclose(mat, expected)
+
def test_matrix_representation(self, tol):
"""Test that the matrix representation is defined correctly"""
diag = np.array([1, -1])
@@ -285,6 +359,15 @@ def test_matrix_representation(self, tol):
assert np.allclose(res_static, expected, atol=tol)
assert np.allclose(res_dynamic, expected, atol=tol)
+ def test_matrix_representation_batched(self, tol):
+ """Test that the matrix representation is defined correctly for a batched diagonal."""
+ diag = np.array([[1, -1, 1j], [-1, -1, -1]])
+ res_static = qml.DiagonalQubitUnitary.compute_matrix(diag)
+ res_dynamic = qml.DiagonalQubitUnitary(diag, wires=0).matrix()
+ expected = np.array([[[1, -1, 1j], [0, 0, 0]], [[0, 0, 0], [-1, -1, -1]]])
+ assert np.allclose(res_static, expected, atol=tol)
+ assert np.allclose(res_dynamic, expected, atol=tol)
+
@pytest.mark.parametrize("n", (2, -1, 0.12345))
@pytest.mark.parametrize("diag", ([1.0, -1.0], np.array([1.0, -1.0])))
def test_pow(self, n, diag):
@@ -296,17 +379,23 @@ def test_pow(self, n, diag):
for x_op, x_pow in zip(op.data[0], pow_ops[0].data[0]):
assert (x_op + 0.0j) ** n == x_pow
- def test_error_matrix_not_unitary(self):
+ @pytest.mark.parametrize("D", [[1, 2], [[0.2, 1.0, -1.0], [1.0, -1j, 1j]]])
+ def test_error_matrix_not_unitary(self, D):
"""Tests that error is raised if diagonal by `compute_matrix` does not lead to a unitary"""
with pytest.raises(ValueError, match="Operator must be unitary"):
- qml.DiagonalQubitUnitary.compute_matrix(np.array([1, 2]))
+ qml.DiagonalQubitUnitary.compute_matrix(np.array(D))
+ with pytest.raises(ValueError, match="Operator must be unitary"):
+ qml.DiagonalQubitUnitary(np.array(D), wires=1).matrix()
- @pytest.mark.jax
- def test_error_eigvals_not_unitary(self):
- """Tests that error is raised by `compute_eigvals` if diagonal does not lead to a unitary"""
+ @pytest.mark.parametrize("D", [[1, 2], [[0.2, 1.0, -1.0], [1.0, -1j, 1j]]])
+ def test_error_eigvals_not_unitary(self, D):
+ """Tests that error is raised if diagonal by `compute_matrix` does not lead to a unitary"""
+ with pytest.raises(ValueError, match="Operator must be unitary"):
+ qml.DiagonalQubitUnitary.compute_eigvals(np.array(D))
with pytest.raises(ValueError, match="Operator must be unitary"):
- qml.DiagonalQubitUnitary.compute_eigvals(np.array([1, 2]))
+ qml.DiagonalQubitUnitary(np.array(D), wires=0).eigvals()
+ # TODO[dwierichs]: Add a JIT test using tensor-batching once devices support it
@pytest.mark.jax
def test_jax_jit(self):
"""Test that the diagonal matrix unitary operation works
@@ -380,6 +469,12 @@ def test_wrong_shape(self):
with pytest.raises(ValueError, match=r"Input unitary must be of shape \(2, 2\)"):
qml.ControlledQubitUnitary(np.eye(4), control_wires=[0, 1], wires=2).matrix()
+ def test_error_batching(self):
+ """Test if ControlledQubitUnitary raises a NotImplementedError when
+ instantiated with a tensor-batched unitary."""
+ with pytest.raises(NotImplementedError, match=r"does not support tensor-batching"):
+ qml.ControlledQubitUnitary(np.ones((4, 4, 3)), control_wires=[0, 1], wires=2)
+
@pytest.mark.parametrize("target_wire", range(3))
def test_toffoli(self, target_wire):
"""Test if ControlledQubitUnitary acts like a Toffoli gate when the input unitary is a
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index 0fbda53a635..ccc19bd2ee3 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -14,6 +14,7 @@
"""
Unit tests for the available built-in parametric qubit operations.
"""
+from functools import reduce
import pytest
import copy
import numpy as np
@@ -55,6 +56,41 @@
qml.DoubleExcitationMinus(0.123, wires=[0, 1, 2, 3]),
]
+BATCHED_OPERATIONS = [
+ qml.RX(np.array([0.142, -0.61, 2.3]), wires=0),
+ qml.RY(np.array([1.291, -0.10, 5.2]), wires=0),
+ qml.RZ(np.array([4.239, -3.21, 1.1]), wires=0),
+ qml.PauliRot(np.array([0.142, -0.61, 2.3]), "Y", wires=0),
+ qml.IsingXX(np.array([0.142, -0.61, 2.3]), wires=[0, 1]),
+ qml.IsingYY(np.array([0.142, -0.61, 2.3]), wires=[0, 1]),
+ qml.IsingZZ(np.array([0.142, -0.61, 2.3]), wires=[0, 1]),
+ qml.Rot(np.array([0.142, -0.61, 2.3]), 0.456, 0.789, wires=0),
+ qml.PhaseShift(np.array([2.12, 0.21, -6.2]), wires=0),
+ qml.ControlledPhaseShift(np.array([1.777, -0.1, 5.29]), wires=[0, 2]),
+ qml.CPhase(np.array([1.777, -0.1, 5.29]), wires=[0, 2]),
+ qml.MultiRZ(np.array([1.124, -2.31, 0.112]), wires=[1, 2, 3]),
+ qml.CRX(np.array([0.836, 0.21, -3.57]), wires=[2, 3]),
+ qml.CRY(np.array([0.721, 2.31, 0.983]), wires=[2, 3]),
+ qml.CRZ(np.array([0.554, 1.11, 2.2]), wires=[2, 3]),
+ qml.U1(np.array([0.142, -0.61, 2.3]), wires=0),
+ qml.U2(np.array([9.23, 1.33, 3.556]), np.array([2.134, 1.2, 0.2]), wires=0),
+ qml.U3(
+ np.array([2.009, 1.33, 3.556]),
+ np.array([2.134, 1.2, 0.2]),
+ np.array([0.78, 0.48, 0.83]),
+ wires=0,
+ ),
+ qml.CRot(
+ np.array([0.142, -0.61, 2.3]),
+ np.array([9.82, 0.2, 0.53]),
+ np.array([0.12, 2.21, 0.789]),
+ wires=[0, 1],
+ ),
+ qml.QubitUnitary(1j * np.array([[[1, 0], [0, -1]], [[0, 1], [1, 0]]]), wires=0),
+ qml.DiagonalQubitUnitary(np.array([[1.0, 1.0j], [1.0j, 1.0j]]), wires=1),
+]
+
+
NON_PARAMETRIZED_OPERATIONS = [
qml.S(wires=0),
qml.SX(wires=0),
@@ -78,9 +114,12 @@
ALL_OPERATIONS = NON_PARAMETRIZED_OPERATIONS + PARAMETRIZED_OPERATIONS
+dot_batched = lambda a, b: np.einsum("ij...,jk...->ik...", a, b)
+multi_dot_batched = lambda matrices: reduce(dot_batched, matrices)
+
class TestOperations:
- @pytest.mark.parametrize("op", ALL_OPERATIONS)
+ @pytest.mark.parametrize("op", ALL_OPERATIONS + BATCHED_OPERATIONS)
def test_parametrized_op_copy(self, op, tol):
"""Tests that copied parametrized ops function as expected"""
copied_op = copy.copy(op)
@@ -100,6 +139,16 @@ def test_adjoint_unitaries(self, op, tol):
np.testing.assert_allclose(res2, np.eye(2 ** len(op.wires)), atol=tol)
assert op.wires == op_d.wires
+ @pytest.mark.parametrize("op", BATCHED_OPERATIONS)
+ def test_adjoint_unitaries_batched(self, op, tol):
+ op_d = op.adjoint()
+ res1 = dot_batched(op.matrix(), op_d.matrix())
+ res2 = dot_batched(op_d.matrix(), op.matrix())
+ I = np.transpose(np.stack([np.eye(2 ** len(op.wires))] * op.matrix().shape[-1]), (1, 2, 0))
+ np.testing.assert_allclose(res1, I, atol=tol)
+ np.testing.assert_allclose(res2, I, atol=tol)
+ assert op.wires == op_d.wires
+
class TestParameterFrequencies:
@pytest.mark.parametrize("op", PARAMETRIZED_OPERATIONS)
@@ -128,9 +177,9 @@ def test_parameter_frequencies_match_generator(self, op, tol):
class TestDecompositions:
- def test_phase_decomposition(self, tol):
+ @pytest.mark.parametrize("phi", [0.3, np.array([0.4, 2.1])])
+ def test_phase_decomposition(self, phi, tol):
"""Tests that the decomposition of the Phase gate is correct"""
- phi = 0.3
op = qml.PhaseShift(phi, wires=0)
res = op.decomposition()
@@ -139,19 +188,26 @@ def test_phase_decomposition(self, tol):
assert res[0].name == "RZ"
assert res[0].wires == Wires([0])
- assert res[0].data[0] == 0.3
+ assert np.allclose(res[0].data[0], phi)
decomposed_matrix = res[0].matrix()
- global_phase = (decomposed_matrix[op.matrix() != 0] / op.matrix()[op.matrix() != 0])[0]
+ phases = decomposed_matrix[op.matrix() != 0] / op.matrix()[op.matrix() != 0]
+ if isinstance(phi, float):
+ global_phase = phases[0]
+ else:
+ global_phase = phases[: len(phi)]
assert np.allclose(decomposed_matrix, global_phase * op.matrix(), atol=tol, rtol=0)
- def test_Rot_decomposition(self):
+ @pytest.mark.parametrize(
+ "phi, theta, omega",
+ [
+ (0.432, 0.654, -5.43),
+ (np.array([0.1, 2.1]), np.array([0.4, -0.2]), np.array([1.1, 0.2])),
+ ],
+ )
+ def test_Rot_decomposition(self, phi, theta, omega):
"""Test the decomposition of Rot."""
- phi = 0.432
- theta = 0.654
- omega = -5.43
-
ops1 = qml.Rot.compute_decomposition(phi, theta, omega, wires=0)
ops2 = qml.Rot(phi, theta, omega, wires=0).decomposition()
@@ -163,12 +219,11 @@ def test_Rot_decomposition(self):
for ops in [ops1, ops2]:
for c, p, op in zip(classes, params, ops):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
- def test_CRX_decomposition(self):
+ @pytest.mark.parametrize("phi", [0.432, np.array([0.1, 2.1])])
+ def test_CRX_decomposition(self, phi):
"""Test the decomposition for CRX."""
- phi = 0.432
-
ops1 = qml.CRX.compute_decomposition(phi, wires=[0, 1])
ops2 = qml.CRX(phi, wires=(0, 1)).decomposition()
@@ -179,13 +234,12 @@ def test_CRX_decomposition(self):
for ops in [ops1, ops2]:
for op, c, p, w in zip(ops, classes, params, wires):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
assert op.wires == w
- def test_CRY_decomposition(self):
+ @pytest.mark.parametrize("phi", [0.432, np.array([0.1, 2.1])])
+ def test_CRY_decomposition(self, phi):
"""Test the decomposition for CRY."""
- phi = 0.432
-
ops1 = qml.CRY.compute_decomposition(phi, wires=[0, 1])
ops2 = qml.CRY(phi, wires=(0, 1)).decomposition()
@@ -196,13 +250,12 @@ def test_CRY_decomposition(self):
for ops in [ops1, ops2]:
for op, c, p, w in zip(ops, classes, params, wires):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
assert op.wires == w
- def test_CRZ_decomposition(self):
+ @pytest.mark.parametrize("phi", [0.432, np.array([0.1, 2.1])])
+ def test_CRZ_decomposition(self, phi):
"""Test the decomposition for CRZ."""
- phi = 0.432
-
ops1 = qml.CRZ.compute_decomposition(phi, wires=[0, 1])
ops2 = qml.CRZ(phi, wires=(0, 1)).decomposition()
@@ -213,10 +266,17 @@ def test_CRZ_decomposition(self):
for ops in [ops1, ops2]:
for op, c, p, w in zip(ops, classes, params, wires):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
assert op.wires == w
- @pytest.mark.parametrize("phi, theta, omega", [[0.5, 0.6, 0.7], [0.1, -0.4, 0.7], [-10, 5, -1]])
+ @pytest.mark.parametrize(
+ "phi, theta, omega",
+ [
+ [0.5, 0.6, 0.7],
+ [-10, 5, -1],
+ [np.array([0.1, 0.2]), np.array([-0.4, 2.19]), np.array([0.7, -0.7])],
+ ],
+ )
def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch):
"""Tests that the decomposition of the CRot gate is correct"""
op = qml.CRot(phi, theta, omega, wires=[0, 1])
@@ -225,46 +285,52 @@ def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch):
mats = []
for i in reversed(res):
if len(i.wires) == 1:
- mats.append(np.kron(np.eye(2), i.matrix()))
+ mat = i.matrix()
+ I = np.eye(2)[:, :, np.newaxis] if len(mat.shape) == 3 else np.eye(2)
+ mats.append(np.kron(I, mat))
else:
mats.append(i.matrix())
- decomposed_matrix = np.linalg.multi_dot(mats)
+ decomposed_matrix = multi_dot_batched(mats)
assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
- def test_U1_decomposition(self):
+ @pytest.mark.parametrize("phi", [0.432, np.array([0.1, 2.1])])
+ def test_U1_decomposition(self, phi):
"""Test the decomposition for U1."""
- phi = 0.432
res = qml.U1(phi, wires=0).decomposition()
res2 = qml.U1.compute_decomposition(phi, wires=0)
assert len(res) == len(res2) == 1
assert res[0].name == res2[0].name == "PhaseShift"
- assert res[0].parameters == res2[0].parameters == [phi]
+ assert np.allclose(res[0].parameters, [phi])
+ assert np.allclose(res2[0].parameters, [phi])
- def test_U2_decomposition(self):
+ @pytest.mark.parametrize(
+ "phi, lam", [(0.432, 0.654), (np.array([0.1, 2.1]), np.array([1.2, 4.9]))]
+ )
+ def test_U2_decomposition(self, phi, lam):
"""Test the decomposition for U2."""
- phi = 0.432
- lam = 0.654
-
ops1 = qml.U2.compute_decomposition(phi, lam, wires=0)
ops2 = qml.U2(phi, lam, wires=0).decomposition()
classes = [qml.Rot, qml.PhaseShift, qml.PhaseShift]
- params = [[lam, np.pi / 2, -lam], [lam], [phi]]
+ params = [[lam, np.ones_like(lam) * np.pi / 2, -lam], [lam], [phi]]
for ops in [ops1, ops2]:
for op, c, p in zip(ops, classes, params):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
- def test_U3_decomposition(self):
+ @pytest.mark.parametrize(
+ "theta, phi, lam",
+ [
+ (0.432, 0.654, 0.218),
+ (np.array([0.1, 2.1]), np.array([1.2, 4.9]), np.array([-1.7, 3.2])),
+ ],
+ )
+ def test_U3_decomposition(self, theta, phi, lam):
"""Test the decomposition for U3."""
- theta = 0.654
- phi = 0.432
- lam = 0.654
-
ops1 = qml.U3.compute_decomposition(theta, phi, lam, wires=0)
ops2 = qml.U3(theta, phi, lam, wires=0).decomposition()
@@ -274,12 +340,12 @@ def test_U3_decomposition(self):
for ops in [ops1, ops2]:
for op, c, p in zip(ops, classes, params):
assert isinstance(op, c)
- assert op.parameters == p
+ assert np.allclose(op.parameters, p)
- def test_isingxx_decomposition(self, tol):
+ @pytest.mark.parametrize("phi", [0.1234, np.array([-0.1, 0.2, 0.5])])
+ def test_isingxx_decomposition(self, phi, tol):
"""Tests that the decomposition of the IsingXX gate is correct"""
- param = 0.1234
- op = qml.IsingXX(param, wires=[3, 2])
+ op = qml.IsingXX(phi, wires=[3, 2])
res = op.decomposition()
assert len(res) == 3
@@ -295,19 +361,21 @@ def test_isingxx_decomposition(self, tol):
mats = []
for i in reversed(res):
if i.wires == Wires([3]):
+ mat = i.matrix()
+ I = np.eye(2)[:, :, np.newaxis] if len(mat.shape) == 3 else np.eye(2)
# RX gate
- mats.append(np.kron(i.matrix(), np.eye(2)))
+ mats.append(np.kron(mat, I))
else:
mats.append(i.matrix())
- decomposed_matrix = np.linalg.multi_dot(mats)
+ decomposed_matrix = multi_dot_batched(mats)
assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
- def test_isingyy_decomposition(self, tol):
+ @pytest.mark.parametrize("phi", [0.1234, np.array([-0.1, 0.2, 0.5])])
+ def test_isingyy_decomposition(self, phi, tol):
"""Tests that the decomposition of the IsingYY gate is correct"""
- param = 0.1234
- op = qml.IsingYY(param, wires=[3, 2])
+ op = qml.IsingYY(phi, wires=[3, 2])
res = op.decomposition()
assert len(res) == 3
@@ -323,19 +391,21 @@ def test_isingyy_decomposition(self, tol):
mats = []
for i in reversed(res):
if i.wires == Wires([3]):
+ mat = i.matrix()
+ I = np.eye(2)[:, :, np.newaxis] if len(mat.shape) == 3 else np.eye(2)
# RY gate
- mats.append(np.kron(i.matrix(), np.eye(2)))
+ mats.append(np.kron(mat, I))
else:
mats.append(i.matrix())
- decomposed_matrix = np.linalg.multi_dot(mats)
+ decomposed_matrix = multi_dot_batched(mats)
assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
- def test_isingzz_decomposition(self, tol):
+ @pytest.mark.parametrize("phi", [0.1234, np.array([-0.1, 0.2, 0.5])])
+ def test_isingzz_decomposition(self, phi, tol):
"""Tests that the decomposition of the IsingZZ gate is correct"""
- param = 0.1234
- op = qml.IsingZZ(param, wires=[3, 2])
+ op = qml.IsingZZ(phi, wires=[3, 2])
res = op.decomposition()
assert len(res) == 3
@@ -351,16 +421,18 @@ def test_isingzz_decomposition(self, tol):
mats = []
for i in reversed(res):
if i.wires == Wires([2]):
+ mat = i.matrix()
+ I = np.eye(2)[:, :, np.newaxis] if len(mat.shape) == 3 else np.eye(2)
# RZ gate
- mats.append(np.kron(np.eye(2), i.matrix()))
+ mats.append(np.kron(I, mat))
else:
mats.append(i.matrix())
- decomposed_matrix = np.linalg.multi_dot(mats)
+ decomposed_matrix = multi_dot_batched(mats)
assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
- @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5])
+ @pytest.mark.parametrize("phi", [-0.1, 0.5, np.array([0.2, -0.9])])
@pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase])
def test_controlled_phase_shift_decomp(self, phi, cphase_op):
"""Tests that the ControlledPhaseShift and CPhase operation
@@ -368,44 +440,17 @@ def test_controlled_phase_shift_decomp(self, phi, cphase_op):
op = cphase_op(phi, wires=[0, 2])
decomp = op.decomposition()
- mats = []
- for i in reversed(decomp):
- if i.wires.tolist() == [0]:
- mats.append(np.kron(i.matrix(), np.eye(4)))
- elif i.wires.tolist() == [1]:
- mats.append(np.kron(np.eye(2), np.kron(i.matrix(), np.eye(2))))
- elif i.wires.tolist() == [2]:
- mats.append(np.kron(np.eye(4), i.matrix()))
- elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 1]:
- mats.append(np.kron(i.matrix(), np.eye(2)))
- elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 2]:
- mats.append(
- np.array(
- [
- [1, 0, 0, 0, 0, 0, 0, 0],
- [0, 1, 0, 0, 0, 0, 0, 0],
- [0, 0, 1, 0, 0, 0, 0, 0],
- [0, 0, 0, 1, 0, 0, 0, 0],
- [0, 0, 0, 0, 0, 1, 0, 0],
- [0, 0, 0, 0, 1, 0, 0, 0],
- [0, 0, 0, 0, 0, 0, 0, 1],
- [0, 0, 0, 0, 0, 0, 1, 0],
- ]
- )
- )
-
- decomposed_matrix = np.linalg.multi_dot(mats)
+ mats = [op.matrix(wire_order=[0, 2]) for op in reversed(decomp)]
+ decomposed_matrix = multi_dot_batched(mats)
lam = np.exp(1j * phi)
+ z = np.zeros_like(lam)
+ o = np.ones_like(lam)
exp = np.array(
[
- [1, 0, 0, 0, 0, 0, 0, 0],
- [0, 1, 0, 0, 0, 0, 0, 0],
- [0, 0, 1, 0, 0, 0, 0, 0],
- [0, 0, 0, 1, 0, 0, 0, 0],
- [0, 0, 0, 0, 1, 0, 0, 0],
- [0, 0, 0, 0, 0, lam, 0, 0],
- [0, 0, 0, 0, 0, 0, 1, 0],
- [0, 0, 0, 0, 0, 0, 0, lam],
+ [o, z, z, z],
+ [z, o, z, z],
+ [z, z, o, z],
+ [z, z, z, lam],
]
)
@@ -418,6 +463,7 @@ def test_phase_shift(self, tol):
# test identity for theta=0
assert np.allclose(qml.PhaseShift.compute_matrix(0), np.identity(2), atol=tol, rtol=0)
+ assert np.allclose(qml.PhaseShift(0, wires=0).matrix(), np.identity(2), atol=tol, rtol=0)
assert np.allclose(qml.U1.compute_matrix(0), np.identity(2), atol=tol, rtol=0)
# test arbitrary phase shift
@@ -426,6 +472,13 @@ def test_phase_shift(self, tol):
assert np.allclose(qml.PhaseShift.compute_matrix(phi), expected, atol=tol, rtol=0)
assert np.allclose(qml.U1.compute_matrix(phi), expected, atol=tol, rtol=0)
+ # test arbitrary batched phase shift
+ phi = 0.5432
+ o, z = np.ones_like(phi), np.zeros_like(phi)
+ expected = np.array([[o, z], [z, np.exp(1j * phi)]])
+ assert np.allclose(qml.PhaseShift.compute_matrix(phi), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.U1.compute_matrix(phi), expected, atol=tol, rtol=0)
+
def test_rx(self, tol):
"""Test x rotation is correct"""
@@ -438,6 +491,12 @@ def test_rx(self, tol):
assert np.allclose(qml.RX.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
assert np.allclose(qml.RX(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0)
+ # test identity for batched theta=pi/2
+ expected = np.tensordot(np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), [1, 1], axes=0)
+ pi_half = np.array([np.pi / 2, np.pi / 2])
+ assert np.allclose(qml.RX.compute_matrix(pi_half), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.RX(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0)
+
# test identity for theta=pi
expected = -1j * np.array([[0, 1], [1, 0]])
assert np.allclose(qml.RX.compute_matrix(np.pi), expected, atol=tol, rtol=0)
@@ -455,6 +514,12 @@ def test_ry(self, tol):
assert np.allclose(qml.RY.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
assert np.allclose(qml.RY(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0)
+ # test identity for batched theta=pi/2
+ expected = np.tensordot(np.array([[1, -1], [1, 1]]) / np.sqrt(2), [1, 1], axes=0)
+ pi_half = np.array([np.pi / 2, np.pi / 2])
+ assert np.allclose(qml.RY.compute_matrix(pi_half), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.RY(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0)
+
# test identity for theta=pi
expected = np.array([[0, -1], [1, 0]])
assert np.allclose(qml.RY.compute_matrix(np.pi), expected, atol=tol, rtol=0)
@@ -472,6 +537,12 @@ def test_rz(self, tol):
assert np.allclose(qml.RZ.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
assert np.allclose(qml.RZ(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0)
+ # test identity for batched theta=pi/2
+ expected = np.tensordot(np.diag(np.exp([-1j * np.pi / 4, 1j * np.pi / 4])), [1, 1], axes=0)
+ pi_half = np.array([np.pi / 2, np.pi / 2])
+ assert np.allclose(qml.RZ.compute_matrix(pi_half), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.RZ(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0)
+
# test identity for theta=pi
assert np.allclose(qml.RZ.compute_matrix(np.pi), -1j * Z, atol=tol, rtol=0)
assert np.allclose(qml.RZ(np.pi, wires=0).matrix(), -1j * Z, atol=tol, rtol=0)
@@ -482,12 +553,14 @@ def test_isingxx(self, tol):
assert np.allclose(qml.IsingXX(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0)
def get_expected(theta):
- expected = np.array(np.diag([np.cos(theta / 2)] * 4), dtype=np.complex128)
+ if len(qml.math.shape(theta)) == 1:
+ expected = np.zeros((4, 4, qml.math.shape(theta)[0]), dtype=np.complex128)
+ else:
+ expected = np.zeros((4, 4), dtype=np.complex128)
+ cos_coeff = np.cos(theta / 2)
sin_coeff = -1j * np.sin(theta / 2)
- expected[3, 0] = sin_coeff
- expected[2, 1] = sin_coeff
- expected[1, 2] = sin_coeff
- expected[0, 3] = sin_coeff
+ expected[0, 0] = expected[1, 1] = expected[2, 2] = expected[3, 3] = cos_coeff
+ expected[3, 0] = expected[2, 1] = expected[1, 2] = expected[0, 3] = sin_coeff
return expected
param = np.pi / 2
@@ -496,6 +569,12 @@ def get_expected(theta):
qml.IsingXX(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0
)
+ param = np.array([np.pi / 2, 0.213])
+ assert np.allclose(qml.IsingXX.compute_matrix(param), get_expected(param), atol=tol, rtol=0)
+ assert np.allclose(
+ qml.IsingXX(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0
+ )
+
param = np.pi
assert np.allclose(qml.IsingXX.compute_matrix(param), get_expected(param), atol=tol, rtol=0)
assert np.allclose(
@@ -511,11 +590,14 @@ def test_isingzz(self, tol):
)
def get_expected(theta):
+ if len(qml.math.shape(theta)) == 1:
+ expected = np.zeros((4, 4, qml.math.shape(theta)[0]), dtype=np.complex128)
+ else:
+ expected = np.zeros((4, 4), dtype=np.complex128)
neg_imag = np.exp(-1j * theta / 2)
- plus_imag = np.exp(1j * theta / 2)
- expected = np.array(
- np.diag([neg_imag, plus_imag, plus_imag, neg_imag]), dtype=np.complex128
- )
+ pos_imag = np.exp(1j * theta / 2)
+ expected[0, 0] = expected[3, 3] = neg_imag
+ expected[1, 1] = expected[2, 2] = pos_imag
return expected
param = np.pi / 2
@@ -527,6 +609,12 @@ def get_expected(theta):
qml.IsingZZ.compute_eigvals(param), np.diagonal(get_expected(param)), atol=tol, rtol=0
)
+ param = np.array([np.pi / 2, 0.213])
+ assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0)
+ assert np.allclose(
+ qml.IsingZZ(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0
+ )
+
param = np.pi
assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0)
assert np.allclose(
@@ -543,16 +631,25 @@ def test_isingzz_matrix_tf(self, tol):
import tensorflow as tf
def get_expected(theta):
+ if len(qml.math.shape(theta)) == 1:
+ expected = np.zeros((4, 4, qml.math.shape(theta)[0]), dtype=np.complex128)
+ else:
+ expected = np.zeros((4, 4), dtype=np.complex128)
neg_imag = np.exp(-1j * theta / 2)
- plus_imag = np.exp(1j * theta / 2)
- expected = np.array(
- np.diag([neg_imag, plus_imag, plus_imag, neg_imag]), dtype=np.complex128
- )
+ pos_imag = np.exp(1j * theta / 2)
+ expected[0, 0] = expected[3, 3] = neg_imag
+ expected[1, 1] = expected[2, 2] = pos_imag
return expected
param = tf.Variable(np.pi)
assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(np.pi), atol=tol, rtol=0)
+ param = np.array([np.pi, 0.1242])
+ param_tf = tf.Variable(param)
+ assert np.allclose(
+ qml.IsingZZ.compute_matrix(param_tf), get_expected(param), atol=tol, rtol=0
+ )
+
def test_Rot(self, tol):
"""Test arbitrary single qubit rotation is correct"""
@@ -580,7 +677,15 @@ def arbitrary_rotation(x, y, z):
qml.Rot(a, b, c, wires=0).matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0
)
- def test_CRx(self, tol):
+ a, b, c = np.array([0.432, -0.124]), np.array([-0.152, 2.912]), np.array([0.9234, -9.2])
+ assert np.allclose(
+ qml.Rot.compute_matrix(a, b, c), arbitrary_rotation(a, b, c), atol=tol, rtol=0
+ )
+ assert np.allclose(
+ qml.Rot(a, b, c, wires=0).matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0
+ )
+
+ def test_CRX(self, tol):
"""Test controlled x rotation is correct"""
# test identity for theta=0
@@ -588,7 +693,7 @@ def test_CRx(self, tol):
assert np.allclose(qml.CRX(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0)
# test identity for theta=pi/2
- expected = np.array(
+ expected_pi_half = np.array(
[
[1, 0, 0, 0],
[0, 1, 0, 0],
@@ -596,13 +701,20 @@ def test_CRx(self, tol):
[0, 0, -1j / np.sqrt(2), 1 / np.sqrt(2)],
]
)
- assert np.allclose(qml.CRX.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRX(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRX.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0)
+ assert np.allclose(
+ qml.CRX(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0
+ )
# test identity for theta=pi
- expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, -1j, 0]])
- assert np.allclose(qml.CRX.compute_matrix(np.pi), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRX(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, -1j, 0]])
+ assert np.allclose(qml.CRX.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0)
+ assert np.allclose(qml.CRX(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0)
+
+ param = np.array([np.pi / 2, np.pi])
+ expected = np.transpose(np.stack([expected_pi_half, expected_pi]), (1, 2, 0))
+ assert np.allclose(qml.CRX.compute_matrix(param), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRX(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
def test_CRY(self, tol):
"""Test controlled y rotation is correct"""
@@ -612,7 +724,7 @@ def test_CRY(self, tol):
assert np.allclose(qml.CRY(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0)
# test identity for theta=pi/2
- expected = np.array(
+ expected_pi_half = np.array(
[
[1, 0, 0, 0],
[0, 1, 0, 0],
@@ -620,13 +732,20 @@ def test_CRY(self, tol):
[0, 0, 1 / np.sqrt(2), 1 / np.sqrt(2)],
]
)
- assert np.allclose(qml.CRY.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRY(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRY.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0)
+ assert np.allclose(
+ qml.CRY(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0
+ )
# test identity for theta=pi
- expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1], [0, 0, 1, 0]])
- assert np.allclose(qml.CRY.compute_matrix(np.pi), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRY(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1], [0, 0, 1, 0]])
+ assert np.allclose(qml.CRY.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0)
+ assert np.allclose(qml.CRY(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0)
+
+ param = np.array([np.pi / 2, np.pi])
+ expected = np.transpose(np.stack([expected_pi_half, expected_pi]), (1, 2, 0))
+ assert np.allclose(qml.CRY.compute_matrix(param), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRY(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
def test_CRZ(self, tol):
"""Test controlled z rotation is correct"""
@@ -636,7 +755,7 @@ def test_CRZ(self, tol):
assert np.allclose(qml.CRZ(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0)
# test identity for theta=pi/2
- expected = np.array(
+ expected_pi_half = np.array(
[
[1, 0, 0, 0],
[0, 1, 0, 0],
@@ -644,13 +763,20 @@ def test_CRZ(self, tol):
[0, 0, 0, np.exp(1j * np.pi / 4)],
]
)
- assert np.allclose(qml.CRZ.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRZ(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRZ.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0)
+ assert np.allclose(
+ qml.CRZ(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0
+ )
# test identity for theta=pi
- expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]])
- assert np.allclose(qml.CRZ.compute_matrix(np.pi), expected, atol=tol, rtol=0)
- assert np.allclose(qml.CRZ(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
+ expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]])
+ assert np.allclose(qml.CRZ.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0)
+ assert np.allclose(qml.CRZ(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0)
+
+ param = np.array([np.pi / 2, np.pi])
+ expected = np.transpose(np.stack([expected_pi_half, expected_pi]), (1, 2, 0))
+ assert np.allclose(qml.CRZ.compute_matrix(param), expected, atol=tol, rtol=0)
+ assert np.allclose(qml.CRZ(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0)
def test_CRot(self, tol):
"""Test controlled arbitrary rotation is correct"""
@@ -674,10 +800,10 @@ def arbitrary_Crotation(x, y, z):
s = np.sin(y / 2)
return np.array(
[
- [1, 0, 0, 0],
- [0, 1, 0, 0],
- [0, 0, np.exp(-0.5j * (x + z)) * c, -np.exp(0.5j * (x - z)) * s],
- [0, 0, np.exp(-0.5j * (x - z)) * s, np.exp(0.5j * (x + z)) * c],
+ [s / s, 0 * s, 0 * s, 0 * s],
+ [0 * s, s / s, 0 * s, 0 * s],
+ [0 * s, 0 * s, np.exp(-0.5j * (x + z)) * c, -np.exp(0.5j * (x - z)) * s],
+ [0 * s, 0 * s, np.exp(-0.5j * (x - z)) * s, np.exp(0.5j * (x + z)) * c],
]
)
@@ -692,22 +818,37 @@ def arbitrary_Crotation(x, y, z):
rtol=0,
)
- def test_U2_gate(self, tol):
+ a, b, c = np.array([0.432, -0.124]), np.array([-0.152, 2.912]), np.array([0.9234, -9.2])
+ assert np.allclose(
+ qml.CRot.compute_matrix(a, b, c), arbitrary_Crotation(a, b, c), atol=tol, rtol=0
+ )
+ assert np.allclose(
+ qml.CRot(a, b, c, wires=[0, 1]).matrix(),
+ arbitrary_Crotation(a, b, c),
+ atol=tol,
+ rtol=0,
+ )
+
+ @pytest.mark.parametrize(
+ "phi, lam", [(0.432, 0.654), (np.array([0.1, 2.1]), np.array([1.2, 4.9]))]
+ )
+ def test_U2_gate(self, phi, lam, tol):
"""Test U2 gate matrix matches the documentation"""
- phi = 0.432
- lam = -0.12
expected = np.array(
- [[1, -np.exp(1j * lam)], [np.exp(1j * phi), np.exp(1j * (phi + lam))]]
+ [[lam / lam, -np.exp(1j * lam)], [np.exp(1j * phi), np.exp(1j * (phi + lam))]]
) / np.sqrt(2)
assert np.allclose(qml.U2.compute_matrix(phi, lam), expected, atol=tol, rtol=0)
assert np.allclose(qml.U2(phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0)
- def test_U3_gate(self, tol):
+ @pytest.mark.parametrize(
+ "theta, phi, lam",
+ [
+ (0.432, 0.654, 0.218),
+ (np.array([0.1, 2.1]), np.array([1.2, 4.9]), np.array([-1.7, 3.2])),
+ ],
+ )
+ def test_U3_gate(self, theta, phi, lam, tol):
"""Test U3 gate matrix matches the documentation"""
- theta = 0.65
- phi = 0.432
- lam = -0.12
-
expected = np.array(
[
[np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)],
@@ -717,7 +858,6 @@ def test_U3_gate(self, tol):
],
]
)
-
assert np.allclose(qml.U3.compute_matrix(theta, phi, lam), expected, atol=tol, rtol=0)
assert np.allclose(qml.U3(theta, phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0)
@@ -734,6 +874,22 @@ def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op):
res = op.eigvals()
assert np.allclose(res, np.diag(exp))
+ @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase])
+ def test_controlled_phase_shift_matrix_and_eigvals_batched(self, cphase_op):
+ """Tests that the ControlledPhaseShift and CPhase operation calculate the
+ correct matrix and eigenvalues for batched parameters"""
+ phi = np.array([0.2, np.pi / 2, -0.1])
+ op = cphase_op(phi, wires=[0, 1])
+ res = op.matrix()
+ o = np.ones_like(phi)
+ z = np.zeros_like(phi)
+ exp = np.array([[o, z, z, z], [z, o, z, z], [z, z, o, z], [z, z, z, np.exp(1j * phi)]])
+ assert np.allclose(res, exp)
+
+ res = op.eigvals()
+ exp_eigvals = np.array([o, o, o, np.exp(1j * phi)])
+ assert np.allclose(res, exp_eigvals)
+
class TestGrad:
device_methods = [
@@ -1212,6 +1368,11 @@ def test_PauliRot_matrix_parametric(self, theta, pauli_word, expected_matrix, to
assert np.allclose(res, expected, atol=tol, rtol=0)
+ res = qml.PauliRot.compute_matrix(np.ones(3) * theta, pauli_word)
+ expected = np.transpose(np.stack([expected_matrix(theta)] * 3), (1, 2, 0))
+
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
@pytest.mark.parametrize(
"theta,pauli_word,expected_matrix",
PAULI_ROT_MATRIX_TEST_DATA,
@@ -1224,6 +1385,11 @@ def test_PauliRot_matrix(self, theta, pauli_word, expected_matrix, tol):
assert np.allclose(res, expected, atol=tol, rtol=0)
+ res = qml.PauliRot.compute_matrix(np.ones(5) * theta, pauli_word)
+ expected = np.transpose(np.stack([expected_matrix] * 5), (1, 2, 0))
+
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
@pytest.mark.parametrize(
"theta,pauli_word,compressed_pauli_word,wires,compressed_wires",
[
@@ -1247,6 +1413,14 @@ def test_PauliRot_matrix_identity(
assert np.allclose(res, expected, atol=tol, rtol=0)
+ batch = np.ones(2) * theta
+ res = qml.PauliRot.compute_matrix(batch, pauli_word)
+ expected = qml.operation.expand_matrix(
+ qml.PauliRot.compute_matrix(batch, compressed_pauli_word), compressed_wires, wires
+ )
+
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
def test_PauliRot_wire_as_int(self):
"""Test that passing a single wire as an integer works."""
@@ -1278,10 +1452,17 @@ def test_PauliRot_all_Identity(self):
assert len(decomp_ops) == 0
- def test_PauliRot_decomposition_ZZ(self):
- """Test that the decomposition for a ZZ rotation is correct."""
+ def test_error_PauliRot_all_Identity_batched(self):
+ """Test handling that tensor-batching is correctly reported as unsupported
+ with the all-identity Pauli."""
+ with pytest.raises(NotImplementedError, match="does not support tensor-batching"):
+ qml.PauliRot(np.ones(2), "II", wires=[0, 1]).matrix()
+ with pytest.raises(NotImplementedError, match="does not support tensor-batching"):
+ qml.PauliRot(np.ones(2), "II", wires=[0, 1]).eigvals()
- theta = 0.4
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_PauliRot_decomposition_ZZ(self, theta):
+ """Test that the decomposition for a ZZ rotation is correct."""
op = qml.PauliRot(theta, "ZZ", wires=[0, 1])
decomp_ops = op.decomposition()
@@ -1290,12 +1471,12 @@ def test_PauliRot_decomposition_ZZ(self):
assert decomp_ops[0].name == "MultiRZ"
assert decomp_ops[0].wires == Wires([0, 1])
- assert decomp_ops[0].data[0] == theta
+ assert np.allclose(decomp_ops[0].data[0], theta)
- def test_PauliRot_decomposition_XY(self):
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_PauliRot_decomposition_XY(self, theta):
"""Test that the decomposition for a XY rotation is correct."""
- theta = 0.4
op = qml.PauliRot(theta, "XY", wires=[0, 1])
decomp_ops = op.decomposition()
@@ -1305,26 +1486,24 @@ def test_PauliRot_decomposition_XY(self):
assert decomp_ops[0].wires == Wires([0])
assert decomp_ops[1].name == "RX"
-
assert decomp_ops[1].wires == Wires([1])
assert decomp_ops[1].data[0] == np.pi / 2
assert decomp_ops[2].name == "MultiRZ"
assert decomp_ops[2].wires == Wires([0, 1])
- assert decomp_ops[2].data[0] == theta
+ assert np.allclose(decomp_ops[2].data[0], theta)
assert decomp_ops[3].name == "Hadamard"
assert decomp_ops[3].wires == Wires([0])
assert decomp_ops[4].name == "RX"
-
assert decomp_ops[4].wires == Wires([1])
assert decomp_ops[4].data[0] == -np.pi / 2
- def test_PauliRot_decomposition_XIYZ(self):
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_PauliRot_decomposition_XIYZ(self, theta):
"""Test that the decomposition for a XIYZ rotation is correct."""
- theta = 0.4
op = qml.PauliRot(theta, "XIYZ", wires=[0, 1, 2, 3])
decomp_ops = op.decomposition()
@@ -1340,7 +1519,7 @@ def test_PauliRot_decomposition_XIYZ(self):
assert decomp_ops[2].name == "MultiRZ"
assert decomp_ops[2].wires == Wires([0, 2, 3])
- assert decomp_ops[2].data[0] == theta
+ assert np.allclose(decomp_ops[2].data[0], theta)
assert decomp_ops[3].name == "Hadamard"
assert decomp_ops[3].wires == Wires([0])
@@ -1361,7 +1540,7 @@ def test_differentiability(self, angle, pauli_word, tol):
def circuit(theta):
qml.PauliRot(theta, pauli_word, wires=[0, 1])
- return qml.expval(qml.PauliZ(0))
+ return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
res = circuit(angle)
gradient = np.squeeze(qml.grad(circuit)(angle))
@@ -1370,6 +1549,29 @@ def circuit(theta):
0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol
)
+ # TODO[dwierichs]: Include this test using tensor-batching once devices support it
+ @pytest.mark.skip("QNodes/Devices do not support tensor-batching yet.")
+ @pytest.mark.parametrize("pauli_word", ["XX", "YY", "ZZ"])
+ def test_differentiability_batched(self, pauli_word, tol):
+ """Test that differentiation of PauliRot works with batched parameters."""
+
+ dev = qml.device("default.qubit", wires=2)
+
+ @qml.qnode(dev)
+ def circuit(theta):
+ qml.PauliRot(theta, pauli_word, wires=[0, 1])
+ return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+ angle = npp.linspace(0, 2 * np.pi, 7, requires_grad=True)
+ res = circuit(angle)
+ jac = qml.jacobian(circuit)(angle)
+
+ assert np.allclose(
+ jac,
+ 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)),
+ atol=tol,
+ )
+
@pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True))
def test_decomposition_integration(self, angle, tol):
"""Test that the decompositon of PauliRot yields the same results."""
@@ -1522,25 +1724,24 @@ def test_MultiRZ_matrix_parametric(self, theta, wires, expected_matrix, tol):
assert np.allclose(res_static, expected, atol=tol, rtol=0)
assert np.allclose(res_dynamic, expected, atol=tol, rtol=0)
- def test_MultiRZ_matrix_expand(self, tol):
- """Test that the MultiRZ matrix respects the wire order."""
+ @pytest.mark.parametrize("num_wires", [1, 2, 3])
+ def test_MultiRZ_matrix_batched(self, num_wires, tol):
+ """Test that the MultiRZ matrix is correct for batched parameters."""
- res = qml.MultiRZ(0.1, wires=[0, 1]).matrix(wire_order=[1, 0])
- expected = np.array(
- [
- [0.99875026 - 0.04997917j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
- [0.0 + 0.0j, 0.99875026 + 0.04997917j, 0.0 + 0.0j, 0.0 + 0.0j],
- [0.0 + 0.0j, 0.0 + 0.0j, 0.99875026 + 0.04997917j, 0.0 + 0.0j],
- [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.99875026 - 0.04997917j],
- ]
- )
+ theta = np.linspace(0, 2 * np.pi, 7)[:3]
+ res_static = qml.MultiRZ.compute_matrix(theta, num_wires)
+ res_dynamic = qml.MultiRZ(theta, wires=list(range(num_wires))).matrix()
+ signs = reduce(np.kron, [np.array([1, -1])] * num_wires) / 2
+ mats = [np.diag(np.exp(-1j * signs * p)) for p in theta]
+ expected = np.transpose(np.stack(mats), (1, 2, 0))
- assert np.allclose(res, expected, atol=tol, rtol=0)
+ assert np.allclose(res_static, expected, atol=tol, rtol=0)
+ assert np.allclose(res_dynamic, expected, atol=tol, rtol=0)
- def test_MultiRZ_decomposition_ZZ(self):
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_MultiRZ_decomposition_ZZ(self, theta):
"""Test that the decomposition for a ZZ rotation is correct."""
- theta = 0.4
op = qml.MultiRZ(theta, wires=[0, 1])
decomp_ops = op.decomposition()
@@ -1550,15 +1751,15 @@ def test_MultiRZ_decomposition_ZZ(self):
assert decomp_ops[1].name == "RZ"
assert decomp_ops[1].wires == Wires([0])
- assert decomp_ops[1].data[0] == theta
+ assert np.allclose(decomp_ops[1].data[0], theta)
assert decomp_ops[2].name == "CNOT"
assert decomp_ops[2].wires == Wires([1, 0])
- def test_MultiRZ_decomposition_ZZZ(self):
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_MultiRZ_decomposition_ZZZ(self, theta):
"""Test that the decomposition for a ZZZ rotation is correct."""
- theta = 0.4
op = qml.MultiRZ(theta, wires=[0, 2, 3])
decomp_ops = op.decomposition()
@@ -1571,7 +1772,7 @@ def test_MultiRZ_decomposition_ZZZ(self):
assert decomp_ops[2].name == "RZ"
assert decomp_ops[2].wires == Wires([0])
- assert decomp_ops[2].data[0] == theta
+ assert np.allclose(decomp_ops[2].data[0], theta)
assert decomp_ops[3].name == "CNOT"
assert decomp_ops[3].wires == Wires([2, 0])
@@ -1599,6 +1800,29 @@ def circuit(theta):
0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol
)
+ # TODO[dwierichs]: Include this test using tensor-batching once devices support it
+ @pytest.mark.skip("QNodes/Devices do not support tensor-batching yet.")
+ def test_differentiability_batched(self, tol):
+ """Test that differentiation of MultiRZ works."""
+
+ dev = qml.device("default.qubit", wires=2)
+
+ @qml.qnode(dev)
+ def circuit(theta):
+ qml.Hadamard(0)
+ qml.Hadamard(1)
+ qml.MultiRZ(theta, wires=[0, 1])
+
+ return qml.expval(qml.PauliX(0) @ qml.PauliX(1))
+
+ angle = npp.linspace(0, 2 * np.pi, 7, requires_grad=True)
+ res = circuit(angle)
+ jac = qml.jacobian(circuit)(angle)
+
+ assert np.allclose(
+ jac, 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol
+ )
+
@pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True))
def test_decomposition_integration(self, angle, tol):
"""Test that the decompositon of MultiRZ yields the same results."""
@@ -1639,6 +1863,28 @@ def test_multirz_generator(self, qubits, mocker):
op.generator()
spy.assert_not_called()
+ @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])])
+ def test_multirz_eigvals(self, theta, tol):
+ """Test that the eigenvalues of the MultiRZ gate are correct."""
+ op = qml.MultiRZ(theta, wires=range(3))
+
+ pos_phase = np.exp(1j * theta / 2)
+ neg_phase = np.exp(-1j * theta / 2)
+ expected = np.array(
+ [
+ neg_phase,
+ pos_phase,
+ pos_phase,
+ neg_phase,
+ pos_phase,
+ neg_phase,
+ neg_phase,
+ pos_phase,
+ ]
+ )
+ eigvals = op.eigvals()
+ assert np.allclose(eigvals, expected)
+
label_data = [
(
@@ -1715,6 +1961,19 @@ def test_multirz_generator(self, qubits, mocker):
),
]
+# labels with batched parameters are not implemented properly yet, the parameters are truncated
+label_data_batched = [
+ (qml.RX(np.array([1.23, 4.56]), wires=0), "RX", "RX", "RX", "RX⁻¹"),
+ (qml.PauliRot(np.array([1.23, 4.5]), "XYZ", wires=(0, 1, 2)), "RXYZ", "RXYZ", "RXYZ", "RXYZ⁻¹"),
+ (
+ qml.U3(np.array([0.1, 0.2]), np.array([-0.1, -0.2]), np.array([1.2, -0.1]), wires=0),
+ "U3",
+ "U3",
+ "U3",
+ "U3⁻¹",
+ ),
+]
+
class TestLabel:
"""Test the label method on parametric ops"""
@@ -1731,6 +1990,18 @@ def test_label_method(self, op, label1, label2, label3, label4):
assert op.label(decimals=0) == label4
op.inv()
+ @pytest.mark.parametrize("op, label1, label2, label3, label4", label_data_batched)
+ def test_label_method_batched(self, op, label1, label2, label3, label4):
+ """Test label method with plain scalers."""
+
+ assert op.label() == label1
+ assert op.label(decimals=2) == label2
+ assert op.label(decimals=0) == label3
+
+ op.inv()
+ assert op.label(decimals=0) == label4
+ op.inv()
+
@pytest.mark.tf
def test_label_tf(self):
"""Test label methods work with tensorflow variables"""
@@ -1786,6 +2057,24 @@ def test_string_parameter(self):
op3 = qml.Rot("x", "y", "z", wires=0)
assert op3.label(decimals=0) == "Rot\n(x,\ny,\nz)"
+ def test_string_parameter_batched(self):
+ """Test labelling works (i.e. does not raise an Error) if variable is a
+ string instead of a float."""
+
+ x = np.array(["x0", "x1", "x2"])
+ y = np.array(["y0", "y1", "y2"])
+ z = np.array(["z0", "z1", "z2"])
+
+ op1 = qml.RX(x, wires=0)
+ assert op1.label() == "RX"
+ assert op1.label(decimals=0) == "RX"
+
+ op2 = qml.CRX(y, wires=(0, 1))
+ assert op2.label(decimals=0) == "RX"
+
+ op3 = qml.Rot(x, y, z, wires=0)
+ assert op3.label(decimals=0) == "Rot"
+
pow_parametric_ops = (
qml.RX(1.234, wires=0),
@@ -1838,14 +2127,14 @@ def test_pow_matrix(self, op, n):
(qml.U2(1.234, 2.345, wires=0), Wires([])),
(qml.U3(1.234, 2.345, 3.456, wires=0), Wires([])),
(qml.IsingXX(1.234, wires=(0, 1)), Wires([])),
- (qml.IsingYY(1.234, wires=(0, 1)), Wires([])),
+ (qml.IsingYY(np.array([-5.1, 0.219]), wires=(0, 1)), Wires([])),
(qml.IsingZZ(1.234, wires=(0, 1)), Wires([])),
### Controlled Ops
(qml.ControlledPhaseShift(1.234, wires=(0, 1)), Wires(0)),
(qml.CPhase(1.234, wires=(0, 1)), Wires(0)),
(qml.CRX(1.234, wires=(0, 1)), Wires(0)),
(qml.CRY(1.234, wires=(0, 1)), Wires(0)),
- (qml.CRZ(1.234, wires=(0, 1)), Wires(0)),
+ (qml.CRZ(np.array([1.234, 0.219]), wires=(0, 1)), Wires(0)),
(qml.CRot(1.234, 2.2345, 3.456, wires=(0, 1)), Wires(0)),
]
diff --git a/tests/test_operation.py b/tests/test_operation.py
index a7a955d4c74..283a46bc253 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -26,12 +26,16 @@
import pennylane as qml
from pennylane.operation import Tensor, operation_derivative, Operator, Operation
-from gate_data import I, X, CNOT
+from gate_data import I, X, CNOT, Toffoli, SWAP, II
from pennylane.wires import Wires
# pylint: disable=no-self-use, no-member, protected-access, pointless-statement
+Toffoli_batched = np.tensordot(Toffoli, [0.1, -4.2j], axes=0)
+CNOT_batched = np.tensordot(CNOT, [1.4], axes=0)
+I_batched = I[:, :, pnp.newaxis]
+
@pytest.mark.parametrize(
"return_type", ("Sample", "Variance", "Expectation", "Probability", "State", "MidMeasure")
@@ -1594,6 +1598,33 @@ def test_pow_undefined(self):
gate.pow(1.234)
+class MyOpWithMat(Operator):
+ num_wires = 1
+
+ @staticmethod
+ def compute_matrix(theta):
+ return np.tensordot(np.array([[0.4, 1.2], [1.2, 0.4]]), theta, axes=0)
+
+
+class TestInheritedRepresentations:
+ """Tests that the default representations allow for
+ inheritance from other representations"""
+
+ def test_eigvals_from_matrix(self):
+ """Test that eigvals can be extracted when a matrix is defined."""
+ # Test with scalar parameter
+ theta = 0.3
+ op = MyOpWithMat(theta, wires=1)
+ eigvals = op.eigvals()
+ assert np.allclose(eigvals, [1.6 * theta, -0.8 * theta])
+
+ # Test with batched parameter
+ theta = np.array([0.3, 0.9, 1.2])
+ op = MyOpWithMat(theta, wires=1)
+ eigvals = op.eigvals()
+ assert np.allclose(eigvals, [1.6 * theta, -0.8 * theta])
+
+
class TestChannel:
"""Unit tests for the Channel class"""
@@ -1831,99 +1862,185 @@ def test_composed(self):
class TestExpandMatrix:
"""Tests for the expand_matrix helper function."""
+ base_matrix_1 = np.arange(1, 5).reshape((2, 2))
+ base_matrix_1_batched = np.arange(1, 13).reshape((2, 2, 3))
+ base_matrix_2 = np.arange(1, 17).reshape((4, 4))
+ base_matrix_2_batched = np.arange(1, 49).reshape((4, 4, 3))
+
def test_no_expansion(self):
"""Tests the case where the original matrix is not changed"""
- base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
- res = qml.operation.expand_matrix(base_matrix, wires=[0, 2], wire_order=[0, 2])
- assert np.allclose(base_matrix, res)
+ res = qml.operation.expand_matrix(self.base_matrix_2, wires=[0, 2], wire_order=[0, 2])
+ assert np.allclose(self.base_matrix_2, res)
+
+ def test_no_expansion_batched(self):
+ """Tests the case where the batched original matrix is not changed"""
+ res = qml.operation.expand_matrix(
+ self.base_matrix_2_batched, wires=[0, 2], wire_order=[0, 2]
+ )
+ assert np.allclose(self.base_matrix_2_batched, res)
def test_permutation(self):
"""Tests the case where the original matrix is permuted"""
- base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
- res = qml.operation.expand_matrix(base_matrix, wires=[0, 2], wire_order=[2, 0])
+ res = qml.operation.expand_matrix(self.base_matrix_2, wires=[0, 2], wire_order=[2, 0])
expected = np.array([[1, 3, 2, 4], [9, 11, 10, 12], [5, 7, 6, 8], [13, 15, 14, 16]])
assert np.allclose(expected, res)
+ def test_permutation_batched(self):
+ """Tests the case where the batched original matrix is permuted"""
+ res = qml.operation.expand_matrix(
+ self.base_matrix_2_batched, wires=[0, 2], wire_order=[2, 0]
+ )
+
+ perm = [0, 2, 1, 3]
+ expected = self.base_matrix_2_batched[perm][:, perm]
+ assert np.allclose(expected, res)
+
def test_expansion(self):
"""Tests the case where the original matrix is expanded"""
- base_matrix = np.array([[0, 1], [1, 0]])
- res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2])
- expected = np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ res = qml.operation.expand_matrix(self.base_matrix_1, wires=[2], wire_order=[0, 2])
+ expected = np.array([[1, 2, 0, 0], [3, 4, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4]])
assert np.allclose(expected, res)
- res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[2, 0])
- expected = np.array([[0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]])
+ res = qml.operation.expand_matrix(self.base_matrix_1, wires=[2], wire_order=[2, 0])
+ expected = np.array([[1, 0, 2, 0], [0, 1, 0, 2], [3, 0, 4, 0], [0, 3, 0, 4]])
assert np.allclose(expected, res)
- @pytest.mark.autograd
- def test_autograd(self, tol):
- """Tests differentiation in autograd by checking how a specific element of the expanded matrix depends on the
- canonical matrix."""
+ def test_expansion_batched(self):
+ """Tests the case where the batched original matrix is expanded"""
+ res = qml.operation.expand_matrix(self.base_matrix_1_batched, wires=[2], wire_order=[0, 2])
+ z = [0, 0, 0]
+ expected = np.array(
+ [
+ [[1, 2, 3], [4, 5, 6], z, z],
+ [[7, 8, 9], [10, 11, 12], z, z],
+ [z, z, [1, 2, 3], [4, 5, 6]],
+ [z, z, [7, 8, 9], [10, 11, 12]],
+ ]
+ )
+ assert np.allclose(expected, res)
- def func(mat):
- res = qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2])
- return res[0, 1]
+ res = qml.operation.expand_matrix(self.base_matrix_1_batched, wires=[2], wire_order=[2, 0])
+ expected = np.array(
+ [
+ [[1, 2, 3], z, [4, 5, 6], z],
+ [z, [1, 2, 3], z, [4, 5, 6]],
+ [[7, 8, 9], z, [10, 11, 12], z],
+ [z, [7, 8, 9], z, [10, 11, 12]],
+ ]
+ )
+ assert np.allclose(expected, res)
- base_matrix = pnp.array([[0.0, 1.0], [1.0, 0.0]], requires_grad=True)
- grad_fn = qml.grad(func)
- gradient = grad_fn(base_matrix)
+ @staticmethod
+ def func_for_autodiff(mat):
+ """Expand a single-qubit matrix to two qubits where the
+ matrix acts on the latter qubit."""
+ return qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2])
+
+ # the entries should be mapped by func_for_autodiff via
+ # source -> destinations
+ # (0, 0) -> (0, 0), (2, 2)
+ # (0, 1) -> (0, 1), (2, 3)
+ # (1, 0) -> (1, 0), (3, 2)
+ # (1, 1) -> (1, 1), (3, 3)
+ # so that the expected Jacobian is 0 everywhere except for the entries
+ # (dest, source) from the above list, where it is 1.
+ expected_autodiff_nobatch = np.zeros((4, 4, 2, 2), dtype=float)
+ indices = [
+ (0, 0, 0, 0),
+ (2, 2, 0, 0),
+ (0, 1, 0, 1),
+ (2, 3, 0, 1),
+ (1, 0, 1, 0),
+ (3, 2, 1, 0),
+ (1, 1, 1, 1),
+ (3, 3, 1, 1),
+ ]
+ for ind in indices:
+ expected_autodiff_nobatch[ind] = 1.0
- # the entry should propagate from position (0, 1) in the original tensor
- expected = np.array([[0.0, 1.0], [0.0, 0.0]])
- assert np.allclose(gradient, expected, atol=tol)
+ # When using tensor-batching, the expected Jacobian
+ # of func_for_autodiff is diagonal in the dimensions 2 and 5
+ expected_autodiff_batched = np.zeros((4, 4, 3, 2, 2, 3), dtype=float)
+ for ind in indices:
+ expected_autodiff_batched[ind[0], ind[1], :, ind[2], ind[3], :] = np.eye(3)
+
+ expected_autodiff = [expected_autodiff_nobatch, expected_autodiff_batched]
+
+ @pytest.mark.autograd
+ @pytest.mark.parametrize(
+ "i, base_matrix",
+ [
+ (0, [[0.2, 1.1], [-1.3, 1.9]]),
+ (1, [[[0.2, 0.5, 1.2], [1.1, -0.3, -0.2]], [[-1.3, 1.9, 0.2], [0.1, 0.2, 0.7]]]),
+ ],
+ )
+ def test_autograd(self, i, base_matrix, tol):
+ """Tests differentiation in autograd by computing the Jacobian of
+ the expanded matrix with respect to the canonical matrix."""
+
+ base_matrix = pnp.array(base_matrix, requires_grad=True)
+ jac_fn = qml.jacobian(self.func_for_autodiff)
+ jac = jac_fn(base_matrix)
+
+ assert np.allclose(jac, self.expected_autodiff[i], atol=tol)
@pytest.mark.torch
- def test_torch(self, tol):
- """Tests differentiation in torch by checking how a specific element of the expanded matrix depends on the
- canonical matrix."""
+ @pytest.mark.parametrize(
+ "i, base_matrix",
+ [
+ (0, [[0.2, 1.1], [-1.3, 1.9]]),
+ (1, [[[0.2, 0.5, 1.2], [1.1, -0.3, -0.2]], [[-1.3, 1.9, 0.2], [0.1, 0.2, 0.7]]]),
+ ],
+ )
+ def test_torch(self, i, base_matrix, tol):
+ """Tests differentiation in torch by computing the Jacobian of
+ the expanded matrix with respect to the canonical matrix."""
import torch
- base_matrix = torch.tensor([[0.0, 1.0], [1.0, 0.0]], requires_grad=True)
- res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2])
- element = res[0, 1]
- element.backward()
- gradient = base_matrix.grad
+ base_matrix = torch.tensor(base_matrix, requires_grad=True)
+ jac = torch.autograd.functional.jacobian(self.func_for_autodiff, base_matrix)
- # the entry should propagate from position (0, 1) in the original tensor
- expected = torch.tensor([[0.0, 1.0], [0.0, 0.0]])
- assert np.allclose(gradient, expected, atol=tol)
+ assert np.allclose(jac, self.expected_autodiff[i], atol=tol)
@pytest.mark.jax
- def test_jax(self, tol):
- """Tests differentiation in jax by checking how a specific element of the expanded matrix depends on the
- canonical matrix."""
+ @pytest.mark.parametrize(
+ "i, base_matrix",
+ [
+ (0, [[0.2, 1.1], [-1.3, 1.9]]),
+ (1, [[[0.2, 0.5, 1.2], [1.1, -0.3, -0.2]], [[-1.3, 1.9, 0.2], [0.1, 0.2, 0.7]]]),
+ ],
+ )
+ def test_jax(self, i, base_matrix, tol):
+ """Tests differentiation in jax by computing the Jacobian of
+ the expanded matrix with respect to the canonical matrix."""
import jax
- from jax import numpy as jnp
-
- def func(mat):
- res = qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2])
- return res[0, 1]
- base_matrix = jnp.array([[0.0, 1.0], [1.0, 0.0]])
- grad_fn = jax.grad(func)
- gradient = grad_fn(base_matrix)
+ base_matrix = jax.numpy.array(base_matrix)
+ jac_fn = jax.jacobian(self.func_for_autodiff)
+ jac = jac_fn(base_matrix)
- # the entry should propagate from position (0, 1) in the original tensor
- expected = np.array([[0.0, 1.0], [0.0, 0.0]])
- assert np.allclose(gradient, expected, atol=tol)
+ assert np.allclose(jac, self.expected_autodiff[i], atol=tol)
@pytest.mark.tf
- def test_tf(self, tol):
- """Tests differentiation in TensorFlow by checking how a specific element of the expanded matrix depends on the
- canonical matrix."""
+ @pytest.mark.parametrize(
+ "i, base_matrix",
+ [
+ (0, [[0.2, 1.1], [-1.3, 1.9]]),
+ (1, [[[0.2, 0.5, 1.2], [1.1, -0.3, -0.2]], [[-1.3, 1.9, 0.2], [0.1, 0.2, 0.7]]]),
+ ],
+ )
+ def test_tf(self, i, base_matrix, tol):
+ """Tests differentiation in TensorFlow by computing the Jacobian of
+ the expanded matrix with respect to the canonical matrix."""
import tensorflow as tf
- base_matrix = tf.Variable([[0.0, 1.0], [1.0, 0.0]])
+ base_matrix = tf.Variable(base_matrix)
with tf.GradientTape() as tape:
- res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2])
- element = res[0, 1]
+ res = self.func_for_autodiff(base_matrix)
- gradient = tape.gradient(element, base_matrix)
-
- # the entry should propagate from position (0, 1) in the original tensor
- expected = tf.constant([[0.0, 1.0], [0.0, 0.0]])
- assert np.allclose(gradient, expected, atol=tol)
+ jac = tape.jacobian(res, base_matrix)
+ assert np.allclose(jac, self.expected_autodiff[i], atol=tol)
def test_expand_one(self, tol):
"""Test that a 1 qubit gate correctly expands to 3 qubits."""
@@ -1948,6 +2065,31 @@ def test_expand_one(self, tol):
expected = np.kron(np.kron(I, I), U)
assert np.allclose(res, expected, atol=tol, rtol=0)
+ def test_expand_one_batched(self, tol):
+ """Test that a batched 1 qubit gate correctly expands to 3 qubits."""
+ U = np.array(
+ [
+ [0.83645892 - 0.40533293j, -0.20215326 + 0.30850569j],
+ [-0.23889780 - 0.28101519j, -0.88031770 - 0.29832709j],
+ ]
+ )
+ # outer product with batch vector
+ U = np.tensordot(U, [0.14, -0.23, 1.3j], axes=0)
+ # test applied to wire 0
+ res = qml.operation.expand_matrix(U, [0], [0, 4, 9])
+ expected = np.kron(np.kron(U, I_batched), I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 4
+ res = qml.operation.expand_matrix(U, [4], [0, 4, 9])
+ expected = np.kron(np.kron(I_batched, U), I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 9
+ res = qml.operation.expand_matrix(U, [9], [0, 4, 9])
+ expected = np.kron(np.kron(I_batched, I_batched), U)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
def test_expand_two_consecutive_wires(self, tol):
"""Test that a 2 qubit gate on consecutive wires correctly
expands to 4 qubits."""
@@ -1968,6 +2110,27 @@ def test_expand_two_consecutive_wires(self, tol):
expected = np.kron(np.kron(I, I), U2)
assert np.allclose(res, expected, atol=tol, rtol=0)
+ def test_expand_two_consecutive_wires_batched(self, tol):
+ """Test that a batched 2 qubit gate on consecutive wires correctly
+ expands to 4 qubits."""
+ U2 = np.array([[0, 1, 1, 1], [1, 0, 1, -1], [1, -1, 0, 1], [1, 1, -1, 0]]) / np.sqrt(3)
+ U2 = np.tensordot(U2, [2.31, 1.53, 0.7 - 1.9j], axes=0)
+
+ # test applied to wire 0+1
+ res = qml.operation.expand_matrix(U2, [0, 1], [0, 1, 2, 3])
+ expected = np.kron(np.kron(U2, I_batched), I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 1+2
+ res = qml.operation.expand_matrix(U2, [1, 2], [0, 1, 2, 3])
+ expected = np.kron(np.kron(I_batched, U2), I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 2+3
+ res = qml.operation.expand_matrix(U2, [2, 3], [0, 1, 2, 3])
+ expected = np.kron(np.kron(I_batched, I_batched), U2)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
def test_expand_two_reversed_wires(self, tol):
"""Test that a 2 qubit gate on reversed consecutive wires correctly
expands to 4 qubits."""
@@ -1977,74 +2140,135 @@ def test_expand_two_reversed_wires(self, tol):
expected = np.kron(np.kron(CNOT[:, rows][rows], I), I)
assert np.allclose(res, expected, atol=tol, rtol=0)
+ def test_expand_two_reversed_wires_batched(self, tol):
+ """Test that a batched 2 qubit gate on reversed consecutive wires correctly
+ expands to 4 qubits."""
+ # CNOT with target on wire 1 and a batch dimension of size 1
+ res = qml.operation.expand_matrix(CNOT_batched, [1, 0], [0, 1, 2, 3])
+ rows = [0, 2, 1, 3]
+ expected = np.kron(np.kron(CNOT_batched[:, rows][rows], I_batched), I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
def test_expand_three_consecutive_wires(self, tol):
"""Test that a 3 qubit gate on consecutive
wires correctly expands to 4 qubits."""
- U_toffoli = np.diag([1 for i in range(8)])
- U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]])
# test applied to wire 0,1,2
- res = qml.operation.expand_matrix(U_toffoli, [0, 1, 2], [0, 1, 2, 3])
- expected = np.kron(U_toffoli, I)
+ res = qml.operation.expand_matrix(Toffoli, [0, 1, 2], [0, 1, 2, 3])
+ expected = np.kron(Toffoli, I)
assert np.allclose(res, expected, atol=tol, rtol=0)
# test applied to wire 1,2,3
- res = qml.operation.expand_matrix(U_toffoli, [1, 2, 3], [0, 1, 2, 3])
- expected = np.kron(I, U_toffoli)
+ res = qml.operation.expand_matrix(Toffoli, [1, 2, 3], [0, 1, 2, 3])
+ expected = np.kron(I, Toffoli)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ def test_expand_three_consecutive_wires_batched(self, tol):
+ """Test that a batched 3 qubit gate on consecutive
+ wires correctly expands to 4 qubits."""
+ # test applied to wire 0,1,2
+ res = qml.operation.expand_matrix(Toffoli_batched, [0, 1, 2], [0, 1, 2, 3])
+ expected = np.kron(Toffoli_batched, I_batched)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 1,2,3
+ res = qml.operation.expand_matrix(Toffoli_batched, [1, 2, 3], [0, 1, 2, 3])
+ expected = np.kron(I_batched, Toffoli_batched)
assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_three_nonconsecutive_ascending_wires(self, tol):
"""Test that a 3 qubit gate on non-consecutive but ascending
wires correctly expands to 4 qubits."""
- U_toffoli = np.diag([1 for i in range(8)])
- U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]])
# test applied to wire 0,2,3
- res = qml.operation.expand_matrix(U_toffoli, [0, 2, 3], [0, 1, 2, 3])
- expected = (
- np.kron(qml.SWAP.compute_matrix(), np.kron(I, I))
- @ np.kron(I, U_toffoli)
- @ np.kron(qml.SWAP.compute_matrix(), np.kron(I, I))
+ res = qml.operation.expand_matrix(Toffoli, [0, 2, 3], [0, 1, 2, 3])
+ expected = np.kron(SWAP, II) @ np.kron(I, Toffoli) @ np.kron(SWAP, II)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 0,1,3
+ res = qml.operation.expand_matrix(Toffoli, [0, 1, 3], [0, 1, 2, 3])
+ expected = np.kron(II, SWAP) @ np.kron(Toffoli, I) @ np.kron(II, SWAP)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ def test_expand_three_nonconsecutive_ascending_wires_batched(self, tol):
+ """Test that a batched 3 qubit gate on non-consecutive but ascending
+ wires correctly expands to 4 qubits."""
+ # test applied to wire 0,2,3
+ res = qml.operation.expand_matrix(Toffoli_batched[:, :, :1], [0, 2, 3], [0, 1, 2, 3])
+ expected = np.tensordot(
+ np.tensordot(
+ np.kron(SWAP, II),
+ np.kron(I_batched, Toffoli_batched[:, :, :1]),
+ axes=[[1], [0]],
+ ),
+ np.kron(SWAP, II),
+ axes=[[1], [0]],
)
+ expected = np.moveaxis(expected, 1, -1)
assert np.allclose(res, expected, atol=tol, rtol=0)
# test applied to wire 0,1,3
- res = qml.operation.expand_matrix(U_toffoli, [0, 1, 3], [0, 1, 2, 3])
- expected = (
- np.kron(np.kron(I, I), qml.SWAP.compute_matrix())
- @ np.kron(U_toffoli, I)
- @ np.kron(np.kron(I, I), qml.SWAP.compute_matrix())
+ res = qml.operation.expand_matrix(Toffoli_batched, [0, 1, 3], [0, 1, 2, 3])
+ _res = qml.operation.expand_matrix(Toffoli, [0, 1, 3], [0, 1, 2, 3])
+ expected = np.tensordot(
+ np.tensordot(
+ np.kron(II, SWAP),
+ np.kron(Toffoli_batched, I_batched),
+ axes=[[1], [0]],
+ ),
+ np.kron(II, SWAP),
+ axes=[[1], [0]],
)
+ expected = np.moveaxis(expected, 1, -1)
assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_three_nonconsecutive_nonascending_wires(self, tol):
"""Test that a 3 qubit gate on non-consecutive non-ascending
wires correctly expands to 4 qubits"""
- U_toffoli = np.diag([1 for i in range(8)])
- U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]])
# test applied to wire 3, 1, 2
- res = qml.operation.expand_matrix(U_toffoli, [3, 1, 2], [0, 1, 2, 3])
+ res = qml.operation.expand_matrix(Toffoli, [3, 1, 2], [0, 1, 2, 3])
+ # change the control qubit on the Toffoli gate
+ rows = [0, 4, 1, 5, 2, 6, 3, 7]
+ Toffoli_perm = Toffoli[:, rows][rows]
+ expected = np.kron(I, Toffoli_perm)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ # test applied to wire 3, 0, 2
+ res = qml.operation.expand_matrix(Toffoli, [3, 0, 2], [0, 1, 2, 3])
+ # change the control qubit on the Toffoli gate
+ expected = np.kron(SWAP, II) @ np.kron(I, Toffoli_perm) @ np.kron(SWAP, II)
+ assert np.allclose(res, expected, atol=tol, rtol=0)
+
+ def test_expand_three_nonconsecutive_nonascending_wires_batched(self, tol):
+ """Test that a batched 3 qubit gate on non-consecutive non-ascending
+ wires correctly expands to 4 qubits"""
+ # test applied to wire 3, 1, 2
+ res = qml.operation.expand_matrix(Toffoli_batched, [3, 1, 2], [0, 1, 2, 3])
# change the control qubit on the Toffoli gate
- rows = np.array([0, 4, 1, 5, 2, 6, 3, 7])
- expected = np.kron(I, U_toffoli[:, rows][rows])
+ rows = [0, 4, 1, 5, 2, 6, 3, 7]
+ Toffoli_batched_perm = Toffoli_batched[:, rows][rows]
+ expected = np.kron(I_batched, Toffoli_batched_perm)
assert np.allclose(res, expected, atol=tol, rtol=0)
# test applied to wire 3, 0, 2
- res = qml.operation.expand_matrix(U_toffoli, [3, 0, 2], [0, 1, 2, 3])
+ res = qml.operation.expand_matrix(Toffoli_batched, [3, 0, 2], [0, 1, 2, 3])
# change the control qubit on the Toffoli gate
- rows = np.array([0, 4, 1, 5, 2, 6, 3, 7])
- expected = (
- np.kron(qml.SWAP.compute_matrix(), np.kron(I, I))
- @ np.kron(I, U_toffoli[:, rows][rows])
- @ np.kron(qml.SWAP.compute_matrix(), np.kron(I, I))
+ expected = np.tensordot(
+ np.tensordot(
+ np.kron(SWAP, II),
+ np.kron(I_batched, Toffoli_batched_perm),
+ axes=[[1], [0]],
+ ),
+ np.kron(SWAP, II),
+ axes=[[1], [0]],
)
+ expected = np.moveaxis(expected, 1, -1)
assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_matrix_usage_in_operator_class(self, tol):
"""Tests that the method is used correctly by defining a dummy operator and
checking the permutation/expansion."""
- base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
-
- permuted_matrix = np.array([[1, 3, 2, 4], [9, 11, 10, 12], [5, 7, 6, 8], [13, 15, 14, 16]])
+ perm = [0, 2, 1, 3]
+ permuted_matrix = self.base_matrix_2[perm][:, perm]
expanded_matrix = np.array(
[
@@ -2063,10 +2287,39 @@ class DummyOp(qml.operation.Operator):
num_wires = 2
def compute_matrix(*params, **hyperparams):
- return base_matrix
+ return self.base_matrix_2
+
+ op = DummyOp(wires=[0, 2])
+ assert np.allclose(op.matrix(), self.base_matrix_2, atol=tol)
+ assert np.allclose(op.matrix(wire_order=[2, 0]), permuted_matrix, atol=tol)
+ assert np.allclose(op.matrix(wire_order=[0, 1, 2]), expanded_matrix, atol=tol)
+
+ def test_expand_matrix_usage_in_operator_class_batched(self, tol):
+ """Tests that the method is used correctly with a batched matrix by defining
+ a dummy operator and checking the permutation/expansion."""
+
+ perm = [0, 2, 1, 3]
+ permuted_matrix = self.base_matrix_2_batched[perm][:, perm]
+
+ expanded_matrix = np.tensordot(
+ np.tensordot(
+ np.kron(SWAP, I),
+ np.kron(I_batched, self.base_matrix_2_batched),
+ axes=[[1], [0]],
+ ),
+ np.kron(SWAP, I),
+ axes=[[1], [0]],
+ )
+ expanded_matrix = np.moveaxis(expanded_matrix, 1, -1)
+
+ class DummyOp(qml.operation.Operator):
+ num_wires = 2
+
+ def compute_matrix(*params, **hyperparams):
+ return self.base_matrix_2_batched
op = DummyOp(wires=[0, 2])
- assert np.allclose(op.matrix(), base_matrix, atol=tol)
+ assert np.allclose(op.matrix(), self.base_matrix_2_batched, atol=tol)
assert np.allclose(op.matrix(wire_order=[2, 0]), permuted_matrix, atol=tol)
assert np.allclose(op.matrix(wire_order=[0, 1, 2]), expanded_matrix, atol=tol)