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)