From ea6699f13861c3b19e45249faad1a19a234bbfe6 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 26 Apr 2022 13:50:11 +0200 Subject: [PATCH 01/12] first supported pipelines --- pennylane/_qubit_device.py | 39 ++++++++++++++++++++---------- pennylane/devices/default_qubit.py | 12 ++++++--- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 652fa4655ae..6d08d4e8ea1 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -552,6 +552,8 @@ def sample_basis_states(self, number_of_states, state_probability): shots = self.shots basis_states = np.arange(number_of_states) + if len(state_probability.shape) > 1: + return np.array([np.random.choice(basis_states, shots, p=prob) for prob in state_probability.T]) return np.random.choice(basis_states, shots, p=state_probability) @staticmethod @@ -608,7 +610,7 @@ def states_to_binary(samples, num_wires, dtype=np.int64): array[int]: basis states in binary representation """ powers_of_two = 1 << np.arange(num_wires, dtype=dtype) - states_sampled_base_ten = samples[:, None] & powers_of_two + states_sampled_base_ten = samples[..., None] & powers_of_two return (states_sampled_base_ten > 0).astype(dtype)[:, ::-1] @property @@ -768,10 +770,15 @@ def marginal_prob(self, prob, wires=None): Returns: array[float]: array of the resulting marginal probabilities. """ + dim = 2**self.num_wires + if prob.size > dim: + batch_dim = prob.size//dim + else: + batch_dim = None if wires is None: # no need to marginalize - return prob + return self._reshape(prob, [dim, batch_dim]) if batch_dim else prob wires = Wires(wires) # determine which subsystems are to be summed over @@ -782,15 +789,21 @@ def marginal_prob(self, prob, wires=None): inactive_device_wires = self.map_wires(inactive_wires) # reshape the probability so that each axis corresponds to a wire - prob = self._reshape(prob, [2] * self.num_wires) + shape = [2] * self.num_wires + if batch_dim: + shape.append(batch_dim) + prob = self._reshape(prob, shape) # sum over all inactive wires # hotfix to catch when default.qubit uses this method # since then device_wires is a list if isinstance(inactive_device_wires, Wires): - prob = self._flatten(self._reduce_sum(prob, inactive_device_wires.labels)) + inactive_device_wires = inactive_device_wires.labels + prob = self._reduce_sum(prob, inactive_device_wires) + if batch_dim: + prob = self._reshape(prob, [-1, batch_dim]) else: - prob = self._flatten(self._reduce_sum(prob, inactive_device_wires)) + prob = self._flatten(prob) # The wires provided might not be in consecutive order (i.e., wires might be [2, 0]). # If this is the case, we must permute the marginalized probability so that @@ -830,7 +843,8 @@ def expval(self, observable, shot_range=None, bin_size=None): # estimate the ev samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.mean(samples, axis=0)) + axis = -1 if bin_size is None else -2 + return np.squeeze(np.mean(samples, axis=axis)) def var(self, observable, shot_range=None, bin_size=None): @@ -867,11 +881,12 @@ def sample(self, observable, shot_range=None, bin_size=None): # translate to wire labels used by device device_wires = self.map_wires(observable.wires) name = observable.name - sample_slice = Ellipsis if shot_range is None else slice(*shot_range) + sample_slice = (Ellipsis,) if shot_range is None else (Ellipsis, slice(*shot_range)) + sub_samples = self._samples[sample_slice] if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: # Process samples for observables with eigenvalues {1, -1} - samples = 1 - 2 * self._samples[sample_slice, device_wires[0]] + samples = 1 - 2 * sub_samples[..., device_wires[0]] elif isinstance( observable, MeasurementProcess @@ -879,17 +894,15 @@ def sample(self, observable, shot_range=None, bin_size=None): if ( len(observable.wires) != 0 ): # if wires are provided, then we only return samples from those wires - samples = self._samples[sample_slice, np.array(device_wires)] + samples = sub_samples[..., np.array(device_wires)] else: - samples = self._samples[sample_slice] + samples = sub_samples else: # Replace the basis state in the computational basis with the correct eigenvalue. # Extract only the columns of the basis samples required based on ``wires``. - samples = self._samples[ - sample_slice, np.array(device_wires) - ] # Add np.array here for Jax support. + samples = sub_samples[..., np.array(device_wires)] # Add np.array here for Jax support. powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] indices = samples @ powers_of_two indices = np.array(indices) # Add np.array here for Jax support. diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index f320bfd9612..7f204bd357d 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -705,7 +705,10 @@ def _apply_unitary(self, state, mat, wires): # translate to wire labels used by device device_wires = self.map_wires(wires) - mat = self._cast(self._reshape(mat, [2] * len(device_wires) * 2), dtype=self.C_DTYPE) + shape = [2] * (len(device_wires) * 2) + if mat.size > 2**(2*len(device_wires)): + shape.append(-1) + mat = self._cast(self._reshape(mat, shape), dtype=self.C_DTYPE) axes = (np.arange(len(device_wires), 2 * len(device_wires)), device_wires) tdot = self._tensordot(mat, state, axes=axes) @@ -735,7 +738,10 @@ def _apply_unitary_einsum(self, state, mat, wires): # translate to wire labels used by device device_wires = self.map_wires(wires) - mat = self._cast(self._reshape(mat, [2] * len(device_wires) * 2), dtype=self.C_DTYPE) + shape = [2] * (len(device_wires) * 2) + if mat.size > 2**(2*len(device_wires)): + shape.append(-1) + mat = self._cast(self._reshape(mat, shape), dtype=self.C_DTYPE) # Tensor indices of the quantum state state_indices = ABC[: self.num_wires] @@ -755,7 +761,7 @@ def _apply_unitary_einsum(self, state, mat, wires): ) # We now put together the indices in the notation numpy's einsum requires - einsum_indices = f"{new_indices}{affected_indices},{state_indices}->{new_state_indices}" + einsum_indices = f"{new_indices}{affected_indices}...,{state_indices}...->{new_state_indices}..." return self._einsum(einsum_indices, mat, state) From 966b35234ca63630cd3c5841b10ae423106c968e Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 28 Apr 2022 17:51:46 +0200 Subject: [PATCH 02/12] allow var --- pennylane/_qubit_device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 6d08d4e8ea1..cb8514a6aa7 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -874,7 +874,8 @@ def var(self, observable, shot_range=None, bin_size=None): # estimate the variance samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - return np.squeeze(np.var(samples, axis=0)) + axis = -1 if bin_size is None else -2 + return np.squeeze(np.mean(samples, axis=axis)) def sample(self, observable, shot_range=None, bin_size=None): From 8a09512b4993e05637fce1dfc1c8db82e71902f2 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 29 Apr 2022 12:13:37 +0200 Subject: [PATCH 03/12] first ops --- pennylane/ops/qubit/attributes.py | 7 +++++++ pennylane/ops/qubit/matrix_ops.py | 30 ++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 62ef3c86cad..4c982c4c5df 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -199,3 +199,10 @@ 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", + ] +) + diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 985d216ee53..e39db42de06 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -65,21 +65,24 @@ 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]), + # TODO: Implement unitarity check also for tensor-batched arguments U + if not (qml.math.is_abstract(U) or len(U_shape)==2 or qml.math.allclose( + qml.math.dot(U, qml.math.transpose(qml.math.conj(U), (1, 0))), + qml.math.eye(dim), atol=1e-6, - ): + )): warnings.warn( f"Operator {U}\n may not be unitary." "Verify unitarity of operation, or use a datatype with increased precision.", @@ -151,7 +154,9 @@ def compute_decomposition(U, wires): return super(QubitUnitary, QubitUnitary).compute_decomposition(U, wires=wires) def adjoint(self): - return QubitUnitary(qml.math.T(qml.math.conj(self.get_matrix())), wires=self.wires) + U = self.get_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 _controlled(self, wire): ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires) @@ -278,6 +283,8 @@ def compute_matrix( target_dim = 2 ** len(u_wires) if len(U) != target_dim: raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}") + if len(qml.math.shape(U))==3: + raise ValueError(f"ControlledQubitUnitary does not support tensor-batching.") # A multi-controlled operation is a block-diagonal matrix partitioned into # blocks where the operation being applied sits in the block positioned at @@ -373,6 +380,9 @@ 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 D]), (2, 0, 1)) + return qml.math.diag(D) @staticmethod @@ -403,9 +413,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( + 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.") return D @@ -434,7 +444,7 @@ def compute_decomposition(D, wires): [QubitUnitary(array([[1, 0], [0, 1]]), wires=[0])] """ - return [QubitUnitary(qml.math.diag(D), wires=wires)] + return [QubitUnitary(self.compute_matrix(D), wires=wires)] def adjoint(self): return DiagonalQubitUnitary(qml.math.conj(self.parameters[0]), wires=self.wires) From 5793311a43be5f6bed2473b9e990ae70060e6259 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Fri, 29 Apr 2022 15:43:46 +0200 Subject: [PATCH 04/12] more ops --- pennylane/_qubit_device.py | 42 ++++++++-------------- pennylane/devices/default_qubit.py | 12 ++----- pennylane/ops/qubit/attributes.py | 8 +++++ pennylane/ops/qubit/matrix_ops.py | 8 ++--- pennylane/ops/qubit/parametric_ops.py | 50 ++++++++++++++++++--------- 5 files changed, 63 insertions(+), 57 deletions(-) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index cb8514a6aa7..652fa4655ae 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -552,8 +552,6 @@ def sample_basis_states(self, number_of_states, state_probability): shots = self.shots basis_states = np.arange(number_of_states) - if len(state_probability.shape) > 1: - return np.array([np.random.choice(basis_states, shots, p=prob) for prob in state_probability.T]) return np.random.choice(basis_states, shots, p=state_probability) @staticmethod @@ -610,7 +608,7 @@ def states_to_binary(samples, num_wires, dtype=np.int64): array[int]: basis states in binary representation """ powers_of_two = 1 << np.arange(num_wires, dtype=dtype) - states_sampled_base_ten = samples[..., None] & powers_of_two + states_sampled_base_ten = samples[:, None] & powers_of_two return (states_sampled_base_ten > 0).astype(dtype)[:, ::-1] @property @@ -770,15 +768,10 @@ def marginal_prob(self, prob, wires=None): Returns: array[float]: array of the resulting marginal probabilities. """ - dim = 2**self.num_wires - if prob.size > dim: - batch_dim = prob.size//dim - else: - batch_dim = None if wires is None: # no need to marginalize - return self._reshape(prob, [dim, batch_dim]) if batch_dim else prob + return prob wires = Wires(wires) # determine which subsystems are to be summed over @@ -789,21 +782,15 @@ def marginal_prob(self, prob, wires=None): inactive_device_wires = self.map_wires(inactive_wires) # reshape the probability so that each axis corresponds to a wire - shape = [2] * self.num_wires - if batch_dim: - shape.append(batch_dim) - prob = self._reshape(prob, shape) + prob = self._reshape(prob, [2] * self.num_wires) # sum over all inactive wires # hotfix to catch when default.qubit uses this method # since then device_wires is a list if isinstance(inactive_device_wires, Wires): - inactive_device_wires = inactive_device_wires.labels - prob = self._reduce_sum(prob, inactive_device_wires) - if batch_dim: - prob = self._reshape(prob, [-1, batch_dim]) + prob = self._flatten(self._reduce_sum(prob, inactive_device_wires.labels)) else: - prob = self._flatten(prob) + prob = self._flatten(self._reduce_sum(prob, inactive_device_wires)) # The wires provided might not be in consecutive order (i.e., wires might be [2, 0]). # If this is the case, we must permute the marginalized probability so that @@ -843,8 +830,7 @@ def expval(self, observable, shot_range=None, bin_size=None): # estimate the ev samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - axis = -1 if bin_size is None else -2 - return np.squeeze(np.mean(samples, axis=axis)) + return np.squeeze(np.mean(samples, axis=0)) def var(self, observable, shot_range=None, bin_size=None): @@ -874,20 +860,18 @@ def var(self, observable, shot_range=None, bin_size=None): # estimate the variance samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) - axis = -1 if bin_size is None else -2 - return np.squeeze(np.mean(samples, axis=axis)) + return np.squeeze(np.var(samples, axis=0)) def sample(self, observable, shot_range=None, bin_size=None): # translate to wire labels used by device device_wires = self.map_wires(observable.wires) name = observable.name - sample_slice = (Ellipsis,) if shot_range is None else (Ellipsis, slice(*shot_range)) - sub_samples = self._samples[sample_slice] + sample_slice = Ellipsis if shot_range is None else slice(*shot_range) if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: # Process samples for observables with eigenvalues {1, -1} - samples = 1 - 2 * sub_samples[..., device_wires[0]] + samples = 1 - 2 * self._samples[sample_slice, device_wires[0]] elif isinstance( observable, MeasurementProcess @@ -895,15 +879,17 @@ def sample(self, observable, shot_range=None, bin_size=None): if ( len(observable.wires) != 0 ): # if wires are provided, then we only return samples from those wires - samples = sub_samples[..., np.array(device_wires)] + samples = self._samples[sample_slice, np.array(device_wires)] else: - samples = sub_samples + samples = self._samples[sample_slice] else: # Replace the basis state in the computational basis with the correct eigenvalue. # Extract only the columns of the basis samples required based on ``wires``. - samples = sub_samples[..., np.array(device_wires)] # Add np.array here for Jax support. + samples = self._samples[ + sample_slice, np.array(device_wires) + ] # Add np.array here for Jax support. powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] indices = samples @ powers_of_two indices = np.array(indices) # Add np.array here for Jax support. diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 7f204bd357d..f320bfd9612 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -705,10 +705,7 @@ def _apply_unitary(self, state, mat, wires): # translate to wire labels used by device device_wires = self.map_wires(wires) - shape = [2] * (len(device_wires) * 2) - if mat.size > 2**(2*len(device_wires)): - shape.append(-1) - mat = self._cast(self._reshape(mat, shape), dtype=self.C_DTYPE) + mat = self._cast(self._reshape(mat, [2] * len(device_wires) * 2), dtype=self.C_DTYPE) axes = (np.arange(len(device_wires), 2 * len(device_wires)), device_wires) tdot = self._tensordot(mat, state, axes=axes) @@ -738,10 +735,7 @@ def _apply_unitary_einsum(self, state, mat, wires): # translate to wire labels used by device device_wires = self.map_wires(wires) - shape = [2] * (len(device_wires) * 2) - if mat.size > 2**(2*len(device_wires)): - shape.append(-1) - mat = self._cast(self._reshape(mat, shape), dtype=self.C_DTYPE) + mat = self._cast(self._reshape(mat, [2] * len(device_wires) * 2), dtype=self.C_DTYPE) # Tensor indices of the quantum state state_indices = ABC[: self.num_wires] @@ -761,7 +755,7 @@ def _apply_unitary_einsum(self, state, mat, wires): ) # We now put together the indices in the notation numpy's einsum requires - einsum_indices = f"{new_indices}{affected_indices}...,{state_indices}...->{new_state_indices}..." + einsum_indices = f"{new_indices}{affected_indices},{state_indices}->{new_state_indices}" return self._einsum(einsum_indices, mat, state) diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 4c982c4c5df..167aaf4246a 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -203,6 +203,14 @@ def __contains__(self, obj): supports_tensorbatching = Attribute( [ "QubitUnitary", + "DiagonalQubitUnitary", + "RX", + "RY", + "RZ", + "PhaseShift", + "ControlledPhaseShift", + "Rot", + "MultiRZ", ] ) diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index e39db42de06..f7f0f2b8c0a 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -71,14 +71,14 @@ def __init__(self, *params, wires, do_queue=True): 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)} or {(dim, dim}, batch_dim) " + 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. # TODO: Implement unitarity check also for tensor-batched arguments U - if not (qml.math.is_abstract(U) or len(U_shape)==2 or qml.math.allclose( + if not (qml.math.is_abstract(U) or len(U_shape)==3 or qml.math.allclose( qml.math.dot(U, qml.math.transpose(qml.math.conj(U), (1, 0))), qml.math.eye(dim), atol=1e-6, @@ -444,14 +444,14 @@ def compute_decomposition(D, wires): [QubitUnitary(array([[1, 0], [0, 1]]), wires=[0])] """ - return [QubitUnitary(self.compute_matrix(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) def _controlled(self, control): DiagonalQubitUnitary( - qml.math.concatenate([np.array([1, 1]), self.parameters[0]]), + qml.math.concatenate([np.ones_like(self.parameters[0]), self.parameters[0]]), wires=Wires(control) + self.wires, ) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 11c0738702e..9e465d8ceed 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -96,12 +96,9 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - - js = -1j * s - - return qml.math.diag([c, c]) + qml.math.stack( - [qml.math.stack([0, js]), qml.math.stack([js, 0])] - ) + + mat = qml.math.stack([qml.math.stack([c, s]), qml.math.stack([s, 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) @@ -177,9 +174,7 @@ 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])] - ) + 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) @@ -254,8 +249,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 @@ -368,9 +364,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 @@ -402,9 +399,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): @@ -512,6 +509,16 @@ def compute_matrix(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) + if len(qml.math.shape(phi))>0: + o = qml.math.ones_like(phi) + z = qml.math.zeros_like(phi) + rows = [ + qml.math.stack([o, z, z, z]), + qml.math.stack([z, o, z, z]), + qml.math.stack([z, z, o, z]), + qml.math.stack([z, z, z, exp_part]), + ] + return qml.math.stack(rows) return qml.math.diag([1, 1, 1, exp_part]) @@ -546,8 +553,9 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) + o = qml.math.ones_like(exp_part) - return qml.math.stack([1, 1, 1, exp_part]) + return qml.math.stack([o, o, o, exp_part]) @staticmethod def compute_decomposition(phi, wires): @@ -803,6 +811,13 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) + if len(qml.math.shape(theta)) > 0: + eigvals = qml.math.exp(qml.math.tensordot(eigs, -1j / 2 * theta, axes=0)) + dim = 2**num_wires + mat = qml.math.zeros((dim, dim, len(theta)), like=1j) + mat[np.diag_indices(dim, ndim=2)] = eigvals + return mat + eigvals = qml.math.exp(-1j * theta / 2 * eigs) return qml.math.diag(eigvals) @@ -844,6 +859,9 @@ def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) + if len(qml.math.shape(theta)) > 0: + return qml.math.exp(qml.math.tensordot(eigs, -1j / 2 * theta, axes=0)) + return qml.math.exp(-1j * theta / 2 * eigs) @staticmethod @@ -1035,7 +1053,7 @@ 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) iden = qml.math.eye(2 ** len(pauli_word)) From bbd9505e0e0b08352046b810ae908140a37edff8 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 3 May 2022 14:36:07 +0200 Subject: [PATCH 05/12] all parametric ops --- pennylane/operation.py | 31 +++- pennylane/ops/qubit/attributes.py | 12 +- pennylane/ops/qubit/matrix_ops.py | 27 ++-- pennylane/ops/qubit/parametric_ops.py | 208 ++++++++++++++++--------- tests/ops/qubit/test_parametric_ops.py | 2 +- 5 files changed, 184 insertions(+), 96 deletions(-) diff --git a/pennylane/operation.py b/pennylane/operation.py index 07fb7d9a103..66758f54daf 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -190,19 +190,22 @@ 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) + shape = qml.math.shape(base_matrix) + batch_dim = shape[-1] if len(shape) == 3 else None interface = qml.math._multi_dispatch(base_matrix) # pylint: disable=protected-access # operator's wire positions relative to wire ordering op_wire_pos = wire_order.indices(wires) I = 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.get_matrix() op_matrix_interface = qml.math.convert_like(base_matrix, I) - 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(I, mat_op_reshaped), axes ) @@ -210,9 +213,14 @@ def expand_matrix(base_matrix, wires, wire_order): 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 @@ -736,7 +744,14 @@ def get_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.get_matrix()) + mat = self.get_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 @@ -852,7 +867,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) @@ -1374,7 +1391,7 @@ def get_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/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 167aaf4246a..24f67b57b78 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -211,6 +211,16 @@ def __contains__(self, obj): "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 f7f0f2b8c0a..74955d57afd 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -78,11 +78,15 @@ def __init__(self, *params, wires, do_queue=True): # 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. # TODO: 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.transpose(qml.math.conj(U), (1, 0))), - qml.math.eye(dim), - atol=1e-6, - )): + if not ( + qml.math.is_abstract(U) + or len(U_shape) == 3 + or qml.math.allclose( + qml.math.dot(U, qml.math.transpose(qml.math.conj(U), (1, 0))), + qml.math.eye(dim), + atol=1e-6, + ) + ): warnings.warn( f"Operator {U}\n may not be unitary." "Verify unitarity of operation, or use a datatype with increased precision.", @@ -155,7 +159,7 @@ def compute_decomposition(U, wires): def adjoint(self): U = self.get_matrix() - axis = (1, 0) if len(qml.math.shape(U))==2 else (1, 0, 2) + 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 _controlled(self, wire): @@ -283,7 +287,7 @@ def compute_matrix( target_dim = 2 ** len(u_wires) if len(U) != target_dim: raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}") - if len(qml.math.shape(U))==3: + if len(qml.math.shape(U)) == 3: raise ValueError(f"ControlledQubitUnitary does not support tensor-batching.") # A multi-controlled operation is a block-diagonal matrix partitioned into @@ -380,7 +384,7 @@ 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: + if len(qml.math.shape(D)) == 2: return qml.math.transpose(qml.math.stack([qml.math.diag(_D) for _D in D]), (2, 0, 1)) return qml.math.diag(D) @@ -413,9 +417,10 @@ def compute_eigvals(D): # pylint: disable=arguments-differ """ D = qml.math.asarray(D) - if not (qml.math.is_abstract(D) or 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.") return D diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 9e465d8ceed..a3c602b40df 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -96,9 +96,11 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - - mat = qml.math.stack([qml.math.stack([c, s]), qml.math.stack([s, c])]) - return mat * qml.math.array([[1+0j, -1j], [-1j, 1+0j]], like=mat) + + c = (1 + 0j) * c + js = -1j * s + 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) @@ -173,7 +175,11 @@ def compute_matrix(theta): # pylint: disable=arguments-differ c = qml.math.cos(theta / 2) s = qml.math.sin(theta / 2) - + 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): @@ -509,16 +515,18 @@ def compute_matrix(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) - if len(qml.math.shape(phi))>0: - o = qml.math.ones_like(phi) - z = qml.math.zeros_like(phi) - rows = [ - qml.math.stack([o, z, z, z]), - qml.math.stack([z, o, z, z]), - qml.math.stack([z, z, o, z]), - qml.math.stack([z, z, z, exp_part]), + 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(rows) + + return qml.math.stack([qml.math.stack(row) for row in matrix]) return qml.math.diag([1, 1, 1, exp_part]) @@ -553,9 +561,9 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) - o = qml.math.ones_like(exp_part) - - return qml.math.stack([o, o, o, exp_part]) + shape = qml.math.shape(phi) + ones = qml.math.ones_like(exp_part) + return qml.math.stack([ones, ones, ones, exp_part]) @staticmethod def compute_decomposition(phi, wires): @@ -811,14 +819,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) - if len(qml.math.shape(theta)) > 0: - eigvals = qml.math.exp(qml.math.tensordot(eigs, -1j / 2 * theta, axes=0)) + 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, len(theta)), like=1j) + mat = qml.math.zeros(((dim, dim) + shape), like=eigvals) mat[np.diag_indices(dim, ndim=2)] = eigvals return mat - eigvals = qml.math.exp(-1j * theta / 2 * eigs) + eigvals = qml.math.exp(-0.5j * theta * eigs) return qml.math.diag(eigvals) def generator(self): @@ -860,9 +869,9 @@ def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ eigs = qml.math.cast_like(eigs, 1j) if len(qml.math.shape(theta)) > 0: - return qml.math.exp(qml.math.tensordot(eigs, -1j / 2 * theta, axes=0)) + return qml.math.exp(qml.math.tensordot(eigs, -0.5j * theta, axes=0)) - return qml.math.exp(-1j * theta / 2 * eigs) + return qml.math.exp(-0.5j * theta * eigs) @staticmethod def compute_decomposition( @@ -1016,7 +1025,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 @@ -1055,7 +1064,11 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ # Simplest case is if the Pauli is the identity matrix if set(pauli_word) == {"I"}: - exp = qml.math.exp(-1j * theta / 2) + if len(qml.math.shape(theta)) > 0: + raise ValueError( + "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 @@ -1082,7 +1095,7 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ return expand( qml.math.dot( qml.math.conj(conjugation_matrix), - qml.math.dot(multi_Z_rot_matrix, conjugation_matrix), + qml.math.tensordot(multi_Z_rot_matrix, conjugation_matrix, axes=[[1], [0]]), ), non_identity_wires, list(range(len(pauli_word))), @@ -1121,8 +1134,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 ValueError( + "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)) @@ -1269,24 +1286,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): @@ -1419,17 +1434,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) - 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])] - ) + 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], + ] + + return qml.math.stack([qml.math.stack(row) for row in matrix]) @staticmethod def compute_decomposition(phi, wires): @@ -1558,9 +1578,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) - return qml.math.diag([1, 1, exp_part, qml.math.conj(exp_part)]) + 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.stack([qml.math.stack(row) for row in matrix]) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -1589,14 +1618,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): @@ -1733,18 +1763,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, ], @@ -1866,9 +1898,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): @@ -1975,7 +2008,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))], ] @@ -2014,8 +2047,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) @@ -2148,8 +2181,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) @@ -2217,15 +2250,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): @@ -2353,14 +2393,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 @@ -2460,10 +2508,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_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index d9671580e2e..b516a340e9c 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -586,7 +586,7 @@ def arbitrary_rotation(x, y, z): qml.Rot(a, b, c, wires=0).get_matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0 ) - def test_CRx(self, tol): + def test_CRX(self, tol): """Test controlled x rotation is correct""" # test identity for theta=0 From d74beef568109e48cc08733a4a00d73f6170bdd2 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 15:11:00 +0200 Subject: [PATCH 06/12] test suite --- pennylane/operation.py | 6 +- pennylane/ops/functions/matrix.py | 3 +- pennylane/ops/qubit/matrix_ops.py | 26 +- pennylane/ops/qubit/parametric_ops.py | 29 +- tests/ops/qubit/test_matrix_ops.py | 163 +++++-- tests/ops/qubit/test_parametric_ops.py | 607 ++++++++++++++++++------- tests/test_operation.py | 414 +++++++++++++---- 7 files changed, 923 insertions(+), 325 deletions(-) diff --git a/pennylane/operation.py b/pennylane/operation.py index eecfba11de4..3544f3c21d3 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -192,7 +192,7 @@ def expand_matrix(base_matrix, wires, wire_order): n = len(wires) shape = qml.math.shape(base_matrix) batch_dim = shape[-1] if len(shape) == 3 else None - interface = qml.math._multi_dispatch(base_matrix) # pylint: disable=protected-access + 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) @@ -209,6 +209,8 @@ def expand_matrix(base_matrix, wires, wire_order): mat_tensordot = qml.math.tensordot( mat_op_reshaped, qml.math.cast_like(I, 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 @@ -815,7 +817,7 @@ def label(self, decimals=None, base_label=None, cache=None): # assume that if the first parameter is matrix-valued, there is only a single parameter # this holds true for all current operations and templates unless tensor-batching # is used - # TODO [dwierichs]: Implement a proper label for tensor-batched operators + # TODO[dwierichs]: Implement a proper label for tensor-batched operators if ( cache is None or not isinstance(cache.get("matrices", None), list) 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/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 73ab51786fe..cc0764a2239 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -77,12 +77,12 @@ def __init__(self, *params, wires, do_queue=True): # 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. - # TODO: Implement unitarity check also for tensor-batched arguments U + # 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.transpose(qml.math.conj(U), (1, 0))), + qml.math.dot(U, qml.math.T(qml.math.conj(U))), qml.math.eye(dim), atol=1e-6, ) @@ -149,12 +149,20 @@ 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 qml.transforms.decompositions.zyz_decomposition(U, Wires(wires)[0]) + return super(QubitUnitary, QubitUnitary).compute_decomposition(U, wires=wires) def adjoint(self): @@ -241,6 +249,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, @@ -287,8 +299,6 @@ def compute_matrix( target_dim = 2 ** len(u_wires) if len(U) != target_dim: raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}") - if len(qml.math.shape(U)) == 3: - raise ValueError(f"ControlledQubitUnitary does not support tensor-batching.") # A multi-controlled operation is a block-diagonal matrix partitioned into # blocks where the operation being applied sits in the block positioned at @@ -385,7 +395,9 @@ def compute_matrix(D): # pylint: disable=arguments-differ 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 D]), (2, 0, 1)) + 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) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index a3c602b40df..0850d431e48 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) @@ -110,7 +111,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): @@ -823,7 +825,7 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ 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) + mat = qml.math.zeros(((dim, dim) + shape), like=eigvals, dtype=complex) mat[np.diag_indices(dim, ndim=2)] = eigvals return mat @@ -1009,7 +1011,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 @@ -1065,7 +1068,7 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ if set(pauli_word) == {"I"}: if len(qml.math.shape(theta)) > 0: - raise ValueError( + raise NotImplementedError( "PauliRot with the identity matrix does not support tensor-batching." ) exp = qml.math.exp(-0.5j * theta) @@ -1091,9 +1094,9 @@ 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( + return expand_matrix( + qml.math.einsum( + "ij,jk...->i...k", qml.math.conj(conjugation_matrix), qml.math.tensordot(multi_Z_rot_matrix, conjugation_matrix, axes=[[1], [0]]), ), @@ -1136,7 +1139,7 @@ def compute_eigvals(theta, pauli_word): # pylint: disable=arguments-differ # Identity must be treated specially because its eigenvalues are all the same if set(pauli_word) == {"I"}: if len(qml.math.shape(theta)) > 0: - raise ValueError( + 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)) @@ -1330,13 +1333,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 @@ -2038,8 +2042,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), ] diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py index 3366e780900..772ba42e73e 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, @@ -35,7 +36,9 @@ class TestQubitUnitary: """Tests for the QubitUnitary class.""" @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.""" @@ -52,18 +55,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.""" @@ -82,18 +88,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.""" @@ -112,17 +121,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.""" @@ -141,18 +153,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 @@ -202,6 +217,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() @@ -220,21 +243,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.""" @@ -247,6 +297,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]) @@ -256,17 +330,22 @@ def test_matrix_representation(self, tol): assert np.allclose(res_static, expected, atol=tol) assert np.allclose(res_dynamic, expected, atol=tol) - def test_error_matrix_not_unitary(self): - """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])) + 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.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_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_eigvals(np.array([1, 2])) + qml.DiagonalQubitUnitary.compute_matrix(np.array(D)) + # 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 @@ -340,6 +419,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 2b8bb5503a0..a0289485d72 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,6 +677,14 @@ def arbitrary_rotation(x, y, z): qml.Rot(a, b, c, wires=0).matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0 ) + 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""" @@ -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(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.""" @@ -1715,6 +1939,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 +1968,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 +2035,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" + control_data = [ (qml.Rot(1, 2, 3, wires=0), Wires([])), @@ -1798,14 +2065,14 @@ def test_string_parameter(self): (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 faa236c9342..1906c166460 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") @@ -1608,99 +1612,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) + + 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) - def func(mat): - res = qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2]) - return res[0, 1] + @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 + + # 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) - 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) + expected_autodiff = [expected_autodiff_nobatch, expected_autodiff_batched] - # 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) + @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 = jax.numpy.array(base_matrix) + jac_fn = jax.jacobian(self.func_for_autodiff) + jac = jac_fn(base_matrix) - base_matrix = jnp.array([[0.0, 1.0], [1.0, 0.0]]) - grad_fn = jax.grad(func) - gradient = grad_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] - - gradient = tape.gradient(element, base_matrix) + res = self.func_for_autodiff(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.""" @@ -1725,6 +1815,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.""" @@ -1745,6 +1860,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.""" @@ -1754,74 +1890,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( [ @@ -1840,10 +2037,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) From a7cc15ea3e83b06002d0480fd3ad1b17b93650e0 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 15:14:19 +0200 Subject: [PATCH 07/12] linting --- pennylane/ops/qubit/matrix_ops.py | 1 - pennylane/ops/qubit/parametric_ops.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index cc0764a2239..5a91b37cf94 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -161,7 +161,6 @@ def compute_decomposition(U, wires): raise DecompositionUndefinedError( "The decomposition of QubitUnitary does not support tensor-batching." ) - return qml.transforms.decompositions.zyz_decomposition(U, Wires(wires)[0]) return super(QubitUnitary, QubitUnitary).compute_decomposition(U, wires=wires) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 0850d431e48..9f9d94d469e 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -563,7 +563,6 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) - shape = qml.math.shape(phi) ones = qml.math.ones_like(exp_part) return qml.math.stack([ones, ones, ones, exp_part]) From f044315dce65671a7f5e9c19f261535d26fb3571 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 15:23:10 +0200 Subject: [PATCH 08/12] changelog --- doc/releases/changelog-dev.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index e233745f1d5..2bdca639709 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,6 +4,28 @@

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) + ``` + * Speed up measuring of commuting Pauli operators [(#2425)](https://github.com/PennyLaneAI/pennylane/pull/2425) From 2b0d8e9037678361f0d3411ac380acc2db5426d9 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 15:37:08 +0200 Subject: [PATCH 09/12] tf device fix PauliRot --- pennylane/ops/qubit/parametric_ops.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 9f9d94d469e..30b7f98b2bc 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -1093,11 +1093,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], ) + 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( - "ij,jk...->i...k", - qml.math.conj(conjugation_matrix), + "jk...,ij->i...k", qml.math.tensordot(multi_Z_rot_matrix, conjugation_matrix, axes=[[1], [0]]), + qml.math.conj(conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), From 51b4c82a79b88b802e84864b470f9688f71cc15f Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 16:00:47 +0200 Subject: [PATCH 10/12] coverage --- tests/test_operation.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_operation.py b/tests/test_operation.py index 1906c166460..78b4f4749bf 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1312,7 +1312,6 @@ class MyOp(Operator): class MyGate(Operation): num_wires = 1 - op = MyOp(wires=1) gate = MyGate(wires=1) @@ -1374,6 +1373,30 @@ def test_generator_undefined(self): with pytest.raises(qml.operation.GeneratorUndefinedError): gate.generator() +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""" From d926eaa055586dc5b12cd9cf356f7ee12995d179 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 16:07:52 +0200 Subject: [PATCH 11/12] coverage --- tests/ops/qubit/test_parametric_ops.py | 24 +++++++++++++++++++++++- tests/test_operation.py | 6 +++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index a0289485d72..c69d649e368 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -1802,7 +1802,7 @@ def circuit(theta): # 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(self, tol): + def test_differentiability_batched(self, tol): """Test that differentiation of MultiRZ works.""" dev = qml.device("default.qubit", wires=2) @@ -1863,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 = [ ( diff --git a/tests/test_operation.py b/tests/test_operation.py index 78b4f4749bf..72ae7dd84f1 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1312,6 +1312,7 @@ class MyOp(Operator): class MyGate(Operation): num_wires = 1 + op = MyOp(wires=1) gate = MyGate(wires=1) @@ -1373,13 +1374,15 @@ def test_generator_undefined(self): with pytest.raises(qml.operation.GeneratorUndefinedError): gate.generator() + 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""" @@ -1398,6 +1401,7 @@ def test_eigvals_from_matrix(self): eigvals = op.eigvals() assert np.allclose(eigvals, [1.6 * theta, -0.8 * theta]) + class TestChannel: """Unit tests for the Channel class""" From 4d4b7061a190557b614d98517f59e999910a1175 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 10 May 2022 20:59:03 +0200 Subject: [PATCH 12/12] coverage DiagonalQubitUnitary --- tests/ops/qubit/test_matrix_ops.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py index 772ba42e73e..39217de507f 100644 --- a/tests/ops/qubit/test_matrix_ops.py +++ b/tests/ops/qubit/test_matrix_ops.py @@ -344,6 +344,16 @@ 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(D)) + with pytest.raises(ValueError, match="Operator must be unitary"): + qml.DiagonalQubitUnitary(np.array(D), wires=1).matrix() + + @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(np.array(D), wires=0).eigvals() # TODO[dwierichs]: Add a JIT test using tensor-batching once devices support it @pytest.mark.jax