From 704122301638a50d14870c69e95a308d524b7eab Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 21 Jun 2022 15:27:09 +0200 Subject: [PATCH 01/16] separated out speedup PR --- doc/releases/changelog-dev.md | 12 +- pennylane/ops/qubit/parametric_ops.py | 170 ++++++++------------------ 2 files changed, 63 insertions(+), 119 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0bb71795c6a..5d0d9059763 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -40,12 +40,18 @@ -1.1258709813834058 ``` -* New PennyLane-inspired `sketch` and `sketch_dark` styles are now available for drawing circuit diagram graphics. +* New PennyLane-inspired `sketch` and `sketch_dark` styles are now available for + drawing circuit diagram graphics. [(#2709)](https://github.com/PennyLaneAI/pennylane/pull/2709)

Improvements

-* Adds a new function to compare operators. `qml.equal` can be used to compare equality of parametric operators taking into account their interfaces and trainability. +* A small performance and readability upgrade to the `compute_matrix` method + of broadcastable parametric operations. + [(#2726)](https://github.com/PennyLaneAI/pennylane/pull/2726) + +* Adds a new function to compare operators. `qml.equal` can be used to compare equality of + parametric operators taking into account their interfaces and trainability. [(#2651)](https://github.com/PennyLaneAI/pennylane/pull/2651)

Breaking changes

@@ -58,4 +64,4 @@ This release contains contributions from (in alphabetical order): -Ankit Khandelwal, Ixchel Meza Chavez, Moritz Willmann +Ankit Khandelwal, Ixchel Meza Chavez, David Wierichs, Moritz Willmann diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 7307b8157f6..3a7e86232a4 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -276,10 +276,9 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - p = qml.math.exp(-0.5j * theta) - z = qml.math.zeros_like(p) - - return qml.math.stack([stack_last([p, z]), stack_last([z, qml.math.conj(p)])], axis=-2) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [-1, 1])) + mat = qml.math.einsum("...i,il->...il", phases, np.eye(2)) + return mat @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -312,9 +311,7 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - p = qml.math.exp(-0.5j * theta) - - return stack_last([p, qml.math.conj(p)]) + return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [-1, 1])) def adjoint(self): return RZ(-self.data[0], wires=self.wires) @@ -399,10 +396,8 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - p = qml.math.exp(1j * phi) - z = qml.math.zeros_like(p) - - return qml.math.stack([stack_last([qml.math.ones_like(p), z]), stack_last([z, p])], axis=-2) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) + return qml.math.einsum("...i,il->...il", phases, np.eye(2)) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -434,9 +429,7 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - p = qml.math.exp(1j * phi) - - return stack_last([qml.math.ones_like(p), p]) + return qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -541,7 +534,7 @@ def compute_matrix(phi): # pylint: disable=arguments-differ **Example** - >>> qml.PhaseShift.compute_matrix(torch.tensor(0.5)) + >>> qml.ControlledPhaseShift.compute_matrix(torch.tensor(0.5)) tensor([[1.0+0.0j, 0.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 1.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 0.0+0.0j, 1.0+0.0j, 0.0000+0.0000j], @@ -550,21 +543,8 @@ 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) - - if qml.math.ndim(phi) > 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([stack_last(row) for row in matrix], axis=-2) - - return qml.math.diag([1, 1, 1, exp_part]) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 0, 0, 1])) + return qml.math.einsum("...i,il->...il", phases, np.eye(4)) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -596,9 +576,7 @@ 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) - ones = qml.math.ones_like(exp_part) - return stack_last([ones, ones, ones, exp_part]) + return qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 0, 0, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -744,12 +722,12 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ mat = [ [ - qml.math.exp(-0.5j * (phi + omega)) * c, - -qml.math.exp(0.5j * (phi - omega)) * s, + qml.math.exp(-1j * (phi + omega) / 2) * c, + -qml.math.exp(1j * (phi - omega) / 2) * s, ], [ - qml.math.exp(-0.5j * (phi - omega)) * s, - qml.math.exp(0.5j * (phi + omega)) * c, + qml.math.exp(-1j * (phi - omega) / 2) * s, + qml.math.exp(1j * (phi + omega) / 2) * c, ], ] @@ -870,12 +848,8 @@ 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 qml.math.ndim(theta) > 0: - eigvals = [qml.math.exp(-0.5j * t * eigs) for t in theta] - return qml.math.stack([qml.math.diag(eig) for eig in eigvals]) - - eigvals = qml.math.exp(-0.5j * theta * eigs) - return qml.math.diag(eigvals) + phases = qml.math.exp(qml.math.einsum("...,i->...i", -0.5j * theta, eigs)) + return qml.math.einsum("...i,il->...il", phases, np.eye(2**num_wires)) def generator(self): return -0.5 * functools.reduce(matmul, [qml.PauliZ(w) for w in self.wires]) @@ -915,10 +889,7 @@ 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 qml.math.ndim(theta) > 0: - return qml.math.exp(qml.math.tensordot(-0.5j * theta, eigs, axes=0)) - - return qml.math.exp(-0.5j * theta * eigs) + return qml.math.exp(qml.math.einsum("...,i->...i", -0.5j * theta, eigs)) @staticmethod def compute_decomposition( @@ -1120,7 +1091,7 @@ 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(-0.5j * theta) + exp = qml.math.exp(-1j * theta / 2) iden = qml.math.eye(2 ** len(pauli_word), like=theta) if qml.math.get_interface(theta) == "tensorflow": iden = qml.math.cast_like(iden, 1j) @@ -1191,7 +1162,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"}: - exp = qml.math.exp(-0.5j * theta) + exp = qml.math.exp(-1j * theta / 2) ones = qml.math.ones(2 ** len(pauli_word), like=theta) if qml.math.get_interface(theta) == "tensorflow": ones = qml.math.cast_like(ones, 1j) @@ -1662,18 +1633,8 @@ 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(-1j * theta / 2) - - ones = qml.math.ones_like(exp_part) - zeros = qml.math.zeros_like(exp_part) - matrix = [ - [ones, zeros, zeros, zeros], - [zeros, ones, zeros, zeros], - [zeros, zeros, exp_part, zeros], - [zeros, zeros, zeros, qml.math.conj(exp_part)], - ] - - return qml.math.stack([stack_last(row) for row in matrix], axis=-2) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [0, 0, -1, 1])) + return qml.math.einsum("...i,il->...il", phases, np.eye(4)) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -1705,10 +1666,7 @@ def compute_eigvals(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) - o = qml.math.ones_like(exp_part) - - return stack_last([o, o, exp_part, qml.math.conj(exp_part)]) + return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [0, 0, -1, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -1865,14 +1823,14 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ [ z, z, - qml.math.exp(-0.5j * (phi + omega)) * c, - -qml.math.exp(0.5j * (phi - omega)) * s, + qml.math.exp(-1j * (phi + omega) / 2) * c, + -qml.math.exp(1j * (phi - omega) / 2) * s, ], [ z, z, - qml.math.exp(-0.5j * (phi - omega)) * s, - qml.math.exp(0.5j * (phi + omega)) * c, + qml.math.exp(-1j * (phi - omega) / 2) * s, + qml.math.exp(1j * (phi + omega) / 2) * c, ], ] @@ -1996,10 +1954,9 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - p = qml.math.exp(1j * phi) - z = qml.math.zeros_like(p) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) - return qml.math.stack([stack_last([qml.math.ones_like(p), z]), stack_last([z, p])], axis=-2) + return qml.math.einsum("...i,il->...il", phases, np.eye(2)) @staticmethod def compute_decomposition(phi, wires): @@ -2383,18 +2340,11 @@ def compute_matrix(phi): # pylint: disable=arguments-differ c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - # The following avoids casting an imaginary quantity to reals when backpropagating - 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([stack_last(row) for row in matrix], axis=-2) + # I[::-1] in conjunction with einsum is not supported by torch + off_I = np.array([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]]) + diag = qml.math.einsum("...,ij->...ij", c, np.eye(4)) + off_diag = qml.math.einsum("...,ij->...ij", -1j * s, off_I) + return diag + off_diag @staticmethod def compute_decomposition(phi, wires): @@ -2542,18 +2492,11 @@ def compute_matrix(phi): # pylint: disable=arguments-differ c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - # The following avoids casting an imaginary quantity to reals when backpropagating - 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([stack_last(row) for row in matrix], axis=-2) + # I[::-1] in conjunction with einsum is not supported by torch + off_I = np.array([[0, 0, 0, 1], [0, 0, -1, 0], [0, -1, 0, 0], [1, 0, 0, 0]]) + diag = qml.math.einsum("...,ij->...ij", c, np.eye(4)) + off_diag = qml.math.einsum("...,ij->...ij", 1j * s, off_I) + return diag + off_diag def adjoint(self): (phi,) = self.parameters @@ -2669,18 +2612,8 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - 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.stack([stack_last(row) for row in matrix], axis=-2) + phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * phi, [-1, 1, 1, -1])) + return qml.math.einsum("...i,il->...il", phases, np.eye(4)) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2749,6 +2682,7 @@ class IsingXY(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: The XY operator satisfies a four-term parameter-shift rule .. math:: @@ -2773,6 +2707,9 @@ class IsingXY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(0.5, 1.0)] @@ -2839,16 +2776,20 @@ def compute_matrix(phi): # pylint: disable=arguments-differ [0. +0.j , 0. +0.24740396j, 0.96891242+0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 1. +0.j ]]) """ - c = qml.math.cos(phi / 2) + c = qml.math.cos(phi / 2) - 1 s = qml.math.sin(phi / 2) - Y = qml.math.convert_like(np.diag([0, 1, 1, 0])[::-1].copy(), phi) + I = qml.math.eye(4, like=c) 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) + I = qml.math.cast_like(I, 1j) - return qml.math.diag([1, c, c, 1]) + 1j * s * Y + mask_c = np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) + mask_s = np.array([[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]]) + diag = qml.math.einsum("...,ij->...ij", c, mask_c) + off_diag = qml.math.einsum("...,ij->...ij", 1j * s, mask_s) + return diag + off_diag + I @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2880,10 +2821,7 @@ def compute_eigvals(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) - - return qml.math.stack([pos_phase, neg_phase, 1, 1]) + return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * phi, [1, -1, 0, 0])) def adjoint(self): (phi,) = self.parameters From 937239317facc08d7b67d85d8119dd805c663aec Mon Sep 17 00:00:00 2001 From: dwierichs Date: Tue, 21 Jun 2022 15:44:54 +0200 Subject: [PATCH 02/16] revert IsingXY change --- pennylane/ops/qubit/parametric_ops.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 3a7e86232a4..0907e47fbc5 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2682,7 +2682,6 @@ class IsingXY(Operation): * Number of wires: 2 * Number of parameters: 1 - * Number of dimensions per parameter: (0,) * Gradient recipe: The XY operator satisfies a four-term parameter-shift rule .. math:: @@ -2707,9 +2706,6 @@ class IsingXY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" - ndim_params = (0,) - """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" - grad_method = "A" parameter_frequencies = [(0.5, 1.0)] @@ -2776,20 +2772,16 @@ def compute_matrix(phi): # pylint: disable=arguments-differ [0. +0.j , 0. +0.24740396j, 0.96891242+0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 1. +0.j ]]) """ - c = qml.math.cos(phi / 2) - 1 + c = qml.math.cos(phi / 2) s = qml.math.sin(phi / 2) - I = qml.math.eye(4, like=c) + Y = qml.math.convert_like(np.diag([0, 1, 1, 0])[::-1].copy(), phi) if qml.math.get_interface(phi) == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - I = qml.math.cast_like(I, 1j) + Y = qml.math.cast_like(Y, 1j) - mask_c = np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]) - mask_s = np.array([[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]]) - diag = qml.math.einsum("...,ij->...ij", c, mask_c) - off_diag = qml.math.einsum("...,ij->...ij", 1j * s, mask_s) - return diag + off_diag + I + return qml.math.diag([1, c, c, 1]) + 1j * s * Y @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2821,7 +2813,10 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * phi, [1, -1, 0, 0])) + pos_phase = qml.math.exp(1.0j * phi / 2) + neg_phase = qml.math.exp(-1.0j * phi / 2) + + return qml.math.stack([pos_phase, neg_phase, 1, 1]) def adjoint(self): (phi,) = self.parameters From fd55ba4a46380fca7ba4e9c4edfd7dfaa2f3ee06 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 22 Jun 2022 01:31:27 +0200 Subject: [PATCH 03/16] alternative version --- pennylane/ops/qubit/parametric_ops.py | 145 +++++++++++++++++--------- 1 file changed, 95 insertions(+), 50 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 0907e47fbc5..b7996e1c9b4 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -276,9 +276,12 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [-1, 1])) - mat = qml.math.einsum("...i,il->...il", phases, np.eye(2)) - return mat + arg = 0.5j * theta + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([-1, 1]))) + + diags = qml.math.exp(qml.math.outer(arg, [-1, 1])) + return diags[:, :, np.newaxis] * np.eye(2) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -311,7 +314,10 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [-1, 1])) + if qml.math.ndim(theta) == 0: + return qml.math.exp(0.5j * theta * np.array([-1, 1])) + + return qml.math.exp(qml.math.outer(0.5j * theta, [-1, 1])) def adjoint(self): return RZ(-self.data[0], wires=self.wires) @@ -396,8 +402,12 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) - return qml.math.einsum("...i,il->...il", phases, np.eye(2)) + arg = 1j * phi + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([0, 1]))) + + diags = qml.math.exp(qml.math.outer(arg, [0, 1])) + return diags[:, :, np.newaxis] * np.eye(2) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -429,7 +439,10 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) + if qml.math.ndim(phi) == 0: + return qml.math.exp(1j * phi * np.array([0, 1])) + + return qml.math.exp(qml.math.outer(1j * phi, [0, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -534,7 +547,7 @@ def compute_matrix(phi): # pylint: disable=arguments-differ **Example** - >>> qml.ControlledPhaseShift.compute_matrix(torch.tensor(0.5)) + >>> qml.PhaseShift.compute_matrix(torch.tensor(0.5)) tensor([[1.0+0.0j, 0.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 1.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 0.0+0.0j, 1.0+0.0j, 0.0000+0.0000j], @@ -543,8 +556,12 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 0, 0, 1])) - return qml.math.einsum("...i,il->...il", phases, np.eye(4)) + arg = 1j * phi + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([0, 0, 0, 1]))) + + diags = qml.math.exp(qml.math.outer(arg, [0, 0, 0, 1])) + return diags[:, :, np.newaxis] * np.eye(4) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -576,7 +593,10 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 0, 0, 1])) + if qml.math.ndim(phi) == 0: + return qml.math.exp(1j * phi * np.array([0, 0, 0, 1])) + + return qml.math.exp(qml.math.outer(1j * phi, [0, 0, 0, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -722,12 +742,12 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ mat = [ [ - qml.math.exp(-1j * (phi + omega) / 2) * c, - -qml.math.exp(1j * (phi - omega) / 2) * s, + qml.math.exp(-0.5j * (phi + omega)) * c, + -qml.math.exp(0.5j * (phi - omega)) * s, ], [ - qml.math.exp(-1j * (phi - omega) / 2) * s, - qml.math.exp(1j * (phi + omega) / 2) * c, + qml.math.exp(-0.5j * (phi - omega)) * s, + qml.math.exp(0.5j * (phi + omega)) * c, ], ] @@ -848,8 +868,11 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", -0.5j * theta, eigs)) - return qml.math.einsum("...i,il->...il", phases, np.eye(2**num_wires)) + if qml.math.ndim(theta) == 0: + return qml.math.diag(qml.math.exp(-0.5j * theta * eigs)) + + diags = qml.math.exp(qml.math.outer(-0.5j * theta, eigs)) + return diags[:, :, np.newaxis] * np.eye(2**num_wires) def generator(self): return -0.5 * functools.reduce(matmul, [qml.PauliZ(w) for w in self.wires]) @@ -889,7 +912,10 @@ def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", -0.5j * theta, eigs)) + if qml.math.ndim(theta) == 0: + return qml.math.exp(-0.5j * theta * eigs) + + return qml.math.exp(qml.math.outer(-0.5j * theta, eigs)) @staticmethod def compute_decomposition( @@ -1091,7 +1117,7 @@ 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) + exp = qml.math.exp(-0.5j * theta) iden = qml.math.eye(2 ** len(pauli_word), like=theta) if qml.math.get_interface(theta) == "tensorflow": iden = qml.math.cast_like(iden, 1j) @@ -1162,7 +1188,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"}: - exp = qml.math.exp(-1j * theta / 2) + exp = qml.math.exp(-0.5j * theta) ones = qml.math.ones(2 ** len(pauli_word), like=theta) if qml.math.get_interface(theta) == "tensorflow": ones = qml.math.cast_like(ones, 1j) @@ -1633,8 +1659,12 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [0, 0, -1, 1])) - return qml.math.einsum("...i,il->...il", phases, np.eye(4)) + arg = 0.5j * theta + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([0, 0, -1, 1]))) + + diags = qml.math.exp(qml.math.outer(arg, [0, 0, -1, 1])) + return diags[:, :, np.newaxis] * np.eye(4) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -1666,7 +1696,10 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - return qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * theta, [0, 0, -1, 1])) + if qml.math.ndim(theta) == 0: + return qml.math.exp(0.5j * theta * np.array([0, 0, -1, 1])) + + return qml.math.exp(qml.math.outer(0.5j * theta, [0, 0, -1, 1])) @staticmethod def compute_decomposition(phi, wires): @@ -1823,14 +1856,14 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ [ z, z, - qml.math.exp(-1j * (phi + omega) / 2) * c, - -qml.math.exp(1j * (phi - omega) / 2) * s, + qml.math.exp(-0.5j * (phi + omega)) * c, + -qml.math.exp(0.5j * (phi - omega)) * s, ], [ z, z, - qml.math.exp(-1j * (phi - omega) / 2) * s, - qml.math.exp(1j * (phi + omega) / 2) * c, + qml.math.exp(-0.5j * (phi - omega)) * s, + qml.math.exp(0.5j * (phi + omega)) * c, ], ] @@ -1954,9 +1987,12 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 1j * phi, [0, 1])) + arg = 1j * phi + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([0, 1]))) - return qml.math.einsum("...i,il->...il", phases, np.eye(2)) + diags = qml.math.exp(qml.math.outer(arg, [0, 1])) + return diags[:, :, np.newaxis] * np.eye(2) @staticmethod def compute_decomposition(phi, wires): @@ -2340,11 +2376,12 @@ def compute_matrix(phi): # pylint: disable=arguments-differ c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - # I[::-1] in conjunction with einsum is not supported by torch - off_I = np.array([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]]) - diag = qml.math.einsum("...,ij->...ij", c, np.eye(4)) - off_diag = qml.math.einsum("...,ij->...ij", -1j * s, off_I) - return diag + off_diag + # The following avoids casting an imaginary quantity to reals when backpropagating + js = -1j * s + if qml.math.ndim(phi) == 0: + return c * np.eye(4) + js * np.eye(4)[::-1] + + return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot(js, np.eye(4)[::-1], axes=0) @staticmethod def compute_decomposition(phi, wires): @@ -2492,11 +2529,11 @@ def compute_matrix(phi): # pylint: disable=arguments-differ c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - # I[::-1] in conjunction with einsum is not supported by torch - off_I = np.array([[0, 0, 0, 1], [0, 0, -1, 0], [0, -1, 0, 0], [1, 0, 0, 0]]) - diag = qml.math.einsum("...,ij->...ij", c, np.eye(4)) - off_diag = qml.math.einsum("...,ij->...ij", 1j * s, off_I) - return diag + off_diag + js = 1j * s + if qml.math.ndim(phi) == 0: + return c * np.eye(4) + js * np.diag([1, -1, -1, 1])[::-1] + + return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot(js, np.diag([1, -1, -1, 1])[::-1], axes=0) def adjoint(self): (phi,) = self.parameters @@ -2612,8 +2649,12 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - phases = qml.math.exp(qml.math.einsum("...,i->...i", 0.5j * phi, [-1, 1, 1, -1])) - return qml.math.einsum("...i,il->...il", phases, np.eye(4)) + arg = 0.5j * phi + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * np.array([-1, 1, 1, -1]))) + + diags = qml.math.exp(qml.math.outer(arg, [-1, 1, 1, -1])) + return diags[:, :, np.newaxis] * np.eye(4) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2645,10 +2686,10 @@ def compute_eigvals(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) + if qml.math.ndim(phi) == 0: + return qml.math.exp(0.5j * phi * np.array([-1, 1, 1, -1])) - return stack_last([neg_phase, pos_phase, pos_phase, neg_phase]) + return qml.math.exp(qml.math.outer(0.5j * phi, [-1, 1, 1, -1])) def adjoint(self): (phi,) = self.parameters @@ -2774,14 +2815,18 @@ 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([0, 1, 1, 0])[::-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([1, c, c, 1]) + 1j * s * Y + js = 1j * s + if qml.math.ndim(phi) == 0: + return qml.math.diag([1, c, c, 1]) + qml.math.diag([0, js, js, 0])[::-1] + + ones = qml.math.ones_like(c) + diags = stack_last([ones, c, c, ones])[:, :, np.newaxis] + return diags * np.eye(4) + qml.math.tensordot(js, np.diag([0, 1, 1, 0])[::-1], axes=0) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2813,10 +2858,10 @@ def compute_eigvals(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) + if qml.math.ndim(phi) == 0: + return qml.math.exp(0.5j * phi * np.array([1, -1, 0, 0])) - return qml.math.stack([pos_phase, neg_phase, 1, 1]) + return qml.math.exp(qml.math.outer(0.5j * phi, [1, -1, 0, 0])) def adjoint(self): (phi,) = self.parameters From a5e3cad31fe189262d2a5a60199aee58a7e929b8 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 22 Jun 2022 01:33:41 +0200 Subject: [PATCH 04/16] black --- 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 b7996e1c9b4..f54750fb876 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2381,7 +2381,9 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.ndim(phi) == 0: return c * np.eye(4) + js * np.eye(4)[::-1] - return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot(js, np.eye(4)[::-1], axes=0) + return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot( + js, np.eye(4)[::-1], axes=0 + ) @staticmethod def compute_decomposition(phi, wires): @@ -2533,7 +2535,9 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.ndim(phi) == 0: return c * np.eye(4) + js * np.diag([1, -1, -1, 1])[::-1] - return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot(js, np.diag([1, -1, -1, 1])[::-1], axes=0) + return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot( + js, np.diag([1, -1, -1, 1])[::-1], axes=0 + ) def adjoint(self): (phi,) = self.parameters From fcc579dbcb5233599904dbb5683389851d05db13 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 22 Jun 2022 20:44:24 +0200 Subject: [PATCH 05/16] more working stuff? --- pennylane/ops/qubit/parametric_ops.py | 85 ++++++++++++++------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index f54750fb876..130abdbd101 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -276,12 +276,12 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - arg = 0.5j * theta - if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([-1, 1]))) + phase = qml.math.exp(0.5j * theta) + if qml.math.ndim(phase) == 0: + return qml.math.diag([qml.math.conj(phase), phase]) - diags = qml.math.exp(qml.math.outer(arg, [-1, 1])) - return diags[:, :, np.newaxis] * np.eye(2) + diags = stack_last([qml.math.conj(phase), phase]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -314,10 +314,8 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - if qml.math.ndim(theta) == 0: - return qml.math.exp(0.5j * theta * np.array([-1, 1])) - - return qml.math.exp(qml.math.outer(0.5j * theta, [-1, 1])) + phase = qml.math.exp(0.5j * theta) + return stack_last([qml.math.conj(phase), phase]) def adjoint(self): return RZ(-self.data[0], wires=self.wires) @@ -404,10 +402,10 @@ def compute_matrix(phi): # pylint: disable=arguments-differ arg = 1j * phi if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([0, 1]))) + return qml.math.diag(qml.math.exp([0, arg])) - diags = qml.math.exp(qml.math.outer(arg, [0, 1])) - return diags[:, :, np.newaxis] * np.eye(2) + diags = qml.math.exp(stack_last([qml.math.zeros_like(arg), arg])) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -439,10 +437,11 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) + arg = 1j * phi if qml.math.ndim(phi) == 0: - return qml.math.exp(1j * phi * np.array([0, 1])) + return qml.math.stack([1, qml.math.exp(arg)]) - return qml.math.exp(qml.math.outer(1j * phi, [0, 1])) + return qml.math.exp(stack_last([qml.math.zeros_like(arg), arg])) @staticmethod def compute_decomposition(phi, wires): @@ -556,12 +555,13 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - arg = 1j * phi - if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([0, 0, 0, 1]))) + phase = qml.math.exp(1j * phi) + if qml.math.ndim(phase) == 0: + return qml.math.diag([0, 0, 0, phase]) - diags = qml.math.exp(qml.math.outer(arg, [0, 0, 0, 1])) - return diags[:, :, np.newaxis] * np.eye(4) + ones = qml.math.ones_like(phase) + diags = stack_last([ones, ones, ones, phase]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -593,10 +593,12 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - if qml.math.ndim(phi) == 0: - return qml.math.exp(1j * phi * np.array([0, 0, 0, 1])) + phase = qml.math.exp(1j * phi) + if qml.math.ndim(phase) == 0: + return qml.math.stack([0, 0, 0, phase]) - return qml.math.exp(qml.math.outer(1j * phi, [0, 0, 0, 1])) + ones = qml.math.ones_like(phase) + return stack_last([ones, ones, ones, phase]) @staticmethod def compute_decomposition(phi, wires): @@ -872,7 +874,7 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ return qml.math.diag(qml.math.exp(-0.5j * theta * eigs)) diags = qml.math.exp(qml.math.outer(-0.5j * theta, eigs)) - return diags[:, :, np.newaxis] * np.eye(2**num_wires) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2**num_wires, like=diags), diags) def generator(self): return -0.5 * functools.reduce(matmul, [qml.PauliZ(w) for w in self.wires]) @@ -1659,12 +1661,13 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - arg = 0.5j * theta - if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([0, 0, -1, 1]))) + phase = qml.math.exp(0.5j * theta) + if qml.math.ndim(theta) == 0: + return qml.math.diag([1, 1, qml.math.conj(phase), phase]) - diags = qml.math.exp(qml.math.outer(arg, [0, 0, -1, 1])) - return diags[:, :, np.newaxis] * np.eye(4) + ones = qml.math.zeros_like(phase) + diags = stack_last([ones, ones, qml.math.conj(phase), phase]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -1696,10 +1699,12 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) + phase = qml.math.exp(0.5j * theta) if qml.math.ndim(theta) == 0: - return qml.math.exp(0.5j * theta * np.array([0, 0, -1, 1])) + return qml.math.stack([1, 1, qml.math.conj(phase), phase]) - return qml.math.exp(qml.math.outer(0.5j * theta, [0, 0, -1, 1])) + ones = qml.math.zeros_like(phase) + return stack_last([ones, ones, qml.math.conj(phase), phase]) @staticmethod def compute_decomposition(phi, wires): @@ -1992,7 +1997,7 @@ def compute_matrix(phi): # pylint: disable=arguments-differ return qml.math.diag(qml.math.exp(arg * np.array([0, 1]))) diags = qml.math.exp(qml.math.outer(arg, [0, 1])) - return diags[:, :, np.newaxis] * np.eye(2) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) @staticmethod def compute_decomposition(phi, wires): @@ -2653,12 +2658,13 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - arg = 0.5j * phi - if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([-1, 1, 1, -1]))) + phase = qml.math.exp(0.5j * phi) + phase_conj = qml.math.conj(phase) + if qml.math.ndim(phi) == 0: + return qml.math.diag([phase_conj, phase, phase, phase_conj]) - diags = qml.math.exp(qml.math.outer(arg, [-1, 1, 1, -1])) - return diags[:, :, np.newaxis] * np.eye(4) + diags = stack_last([phase_conj, phase, phase, phase_conj]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2690,10 +2696,9 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - if qml.math.ndim(phi) == 0: - return qml.math.exp(0.5j * phi * np.array([-1, 1, 1, -1])) - - return qml.math.exp(qml.math.outer(0.5j * phi, [-1, 1, 1, -1])) + phase = qml.math.exp(0.5j * phi) + phase_conj = qml.math.conj(phase) + return stack_last([phase_conj, phase, phase, phase_conj]) def adjoint(self): (phi,) = self.parameters From 9fcfd09d753cfecff9357a09832393aa2b4a6dbe Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 7 Jul 2022 11:58:06 +0200 Subject: [PATCH 06/16] tmp --- pennylane/ops/qubit/parametric_ops.py | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 130abdbd101..65d3a94131c 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -252,6 +252,41 @@ def generator(self): def __init__(self, phi, wires, do_queue=True, id=None): super().__init__(phi, wires=wires, do_queue=do_queue, id=id) + @staticmethod + def compute_matrix_new(theta): # pylint: disable=arguments-differ + r"""Representation of the operator as a canonical matrix in the computational basis (static method). + + The canonical matrix is the textbook matrix representation that does not consider wires. + Implicitly, this assumes that the wires of the operator correspond to the global wire order. + + .. seealso:: :meth:`~.RZ.matrix` + + Args: + theta (tensor_like or float): rotation angle + + Returns: + tensor_like: canonical matrix + + **Example** + + >>> qml.RZ.compute_matrix(torch.tensor(0.5)) + tensor([[0.9689-0.2474j, 0.0000+0.0000j], + [0.0000+0.0000j, 0.9689+0.2474j]]) + """ + if qml.math.get_interface(theta)=="tensorflow": + theta = qml.math.cast_like(theta, 1j) + signs = qml.math.cast_like([-1, 1], 1j) + else: + signs = qml.math.array([-1, 1], like=theta) + arg = 0.5j * theta + + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * signs)) + + diags = qml.math.exp(qml.math.outer(arg, signs)) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) + + @staticmethod def compute_matrix(theta): # pylint: disable=arguments-differ r"""Representation of the operator as a canonical matrix in the computational basis (static method). From a47412c2a84dcd1428e84de9b6e505bec65f6203 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 7 Jul 2022 12:05:12 +0200 Subject: [PATCH 07/16] converged RZ.compute_matrix --- pennylane/ops/qubit/parametric_ops.py | 44 +++++---------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 65d3a94131c..3a1479f6c68 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -253,7 +253,7 @@ def __init__(self, phi, wires, do_queue=True, id=None): super().__init__(phi, wires=wires, do_queue=do_queue, id=id) @staticmethod - def compute_matrix_new(theta): # pylint: disable=arguments-differ + def compute_matrix(theta): # pylint: disable=arguments-differ r"""Representation of the operator as a canonical matrix in the computational basis (static method). The canonical matrix is the textbook matrix representation that does not consider wires. @@ -274,10 +274,12 @@ def compute_matrix_new(theta): # pylint: disable=arguments-differ [0.0000+0.0000j, 0.9689+0.2474j]]) """ if qml.math.get_interface(theta)=="tensorflow": - theta = qml.math.cast_like(theta, 1j) - signs = qml.math.cast_like([-1, 1], 1j) - else: - signs = qml.math.array([-1, 1], like=theta) + p = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) + z = qml.math.zeros_like(p) + + return qml.math.stack([stack_last([p, z]), stack_last([z, qml.math.conj(p)])], axis=-2) + + signs = qml.math.array([-1, 1], like=theta) arg = 0.5j * theta if qml.math.ndim(arg) == 0: @@ -286,38 +288,6 @@ def compute_matrix_new(theta): # pylint: disable=arguments-differ diags = qml.math.exp(qml.math.outer(arg, signs)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) - - @staticmethod - def compute_matrix(theta): # pylint: disable=arguments-differ - r"""Representation of the operator as a canonical matrix in the computational basis (static method). - - The canonical matrix is the textbook matrix representation that does not consider wires. - Implicitly, this assumes that the wires of the operator correspond to the global wire order. - - .. seealso:: :meth:`~.RZ.matrix` - - Args: - theta (tensor_like or float): rotation angle - - Returns: - tensor_like: canonical matrix - - **Example** - - >>> qml.RZ.compute_matrix(torch.tensor(0.5)) - tensor([[0.9689-0.2474j, 0.0000+0.0000j], - [0.0000+0.0000j, 0.9689+0.2474j]]) - """ - if qml.math.get_interface(theta) == "tensorflow": - theta = qml.math.cast_like(theta, 1j) - - phase = qml.math.exp(0.5j * theta) - if qml.math.ndim(phase) == 0: - return qml.math.diag([qml.math.conj(phase), phase]) - - diags = stack_last([qml.math.conj(phase), phase]) - return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) - @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ r"""Eigenvalues of the operator in the computational basis (static method). From f1b13066fdabde37798e6a951a17d6fa4eb46a88 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 7 Jul 2022 12:13:32 +0200 Subject: [PATCH 08/16] converged compute_eigvals --- pennylane/ops/qubit/parametric_ops.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 3a1479f6c68..ebcbf9684b0 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -315,12 +315,16 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ >>> qml.RZ.compute_eigvals(torch.tensor(0.5)) tensor([0.9689-0.2474j, 0.9689+0.2474j]) """ + if qml.math.get_interface(theta)=="tensorflow": + phase = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) + return qml.math.stack([phase, qml.math.conj(phase)], axis=-1) - if qml.math.get_interface(theta) == "tensorflow": - theta = qml.math.cast_like(theta, 1j) - - phase = qml.math.exp(0.5j * theta) - return stack_last([qml.math.conj(phase), phase]) + prefactors = qml.math.array([-0.5j, 0.5j], like=theta) + if qml.math.ndim(theta) == 0: + product = theta * prefactors + else: + product = qml.math.outer(theta, prefactors) + return qml.math.exp(product) def adjoint(self): return RZ(-self.data[0], wires=self.wires) From ca75af9b6288617842a10ea255f46fddef8d63f7 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 7 Jul 2022 13:28:35 +0200 Subject: [PATCH 09/16] tests --- pennylane/ops/qubit/parametric_ops.py | 14 ++++++++------ tests/ops/qubit/test_parametric_ops.py | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index ebcbf9684b0..7f928d76a9d 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -273,7 +273,7 @@ def compute_matrix(theta): # pylint: disable=arguments-differ tensor([[0.9689-0.2474j, 0.0000+0.0000j], [0.0000+0.0000j, 0.9689+0.2474j]]) """ - if qml.math.get_interface(theta)=="tensorflow": + if qml.math.get_interface(theta) == "tensorflow": p = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) z = qml.math.zeros_like(p) @@ -315,7 +315,7 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ >>> qml.RZ.compute_eigvals(torch.tensor(0.5)) tensor([0.9689-0.2474j, 0.9689+0.2474j]) """ - if qml.math.get_interface(theta)=="tensorflow": + if qml.math.get_interface(theta) == "tensorflow": phase = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) return qml.math.stack([phase, qml.math.conj(phase)], axis=-1) @@ -566,7 +566,7 @@ def compute_matrix(phi): # pylint: disable=arguments-differ phase = qml.math.exp(1j * phi) if qml.math.ndim(phase) == 0: - return qml.math.diag([0, 0, 0, phase]) + return qml.math.diag([1, 1, 1, phase]) ones = qml.math.ones_like(phase) diags = stack_last([ones, ones, ones, phase]) @@ -604,7 +604,7 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ phase = qml.math.exp(1j * phi) if qml.math.ndim(phase) == 0: - return qml.math.stack([0, 0, 0, phase]) + return qml.math.stack([1, 1, 1, phase]) ones = qml.math.ones_like(phase) return stack_last([ones, ones, ones, phase]) @@ -883,7 +883,9 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ return qml.math.diag(qml.math.exp(-0.5j * theta * eigs)) diags = qml.math.exp(qml.math.outer(-0.5j * theta, eigs)) - return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2**num_wires, like=diags), diags) + return diags[:, :, np.newaxis] * qml.math.cast_like( + qml.math.eye(2**num_wires, like=diags), diags + ) def generator(self): return -0.5 * functools.reduce(matmul, [qml.PauliZ(w) for w in self.wires]) @@ -1674,7 +1676,7 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.ndim(theta) == 0: return qml.math.diag([1, 1, qml.math.conj(phase), phase]) - ones = qml.math.zeros_like(phase) + ones = qml.math.ones_like(phase) diags = stack_last([ones, ones, qml.math.conj(phase), phase]) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index ccb27853e24..0c4c2911953 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -1491,7 +1491,7 @@ def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op): assert np.allclose(res, exp) res = op.eigvals() - assert np.allclose(res, np.diag(exp)) + assert np.allclose(np.diag(res), exp) @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) def test_controlled_phase_shift_matrix_and_eigvals_broadcasted(self, cphase_op): @@ -1965,7 +1965,7 @@ def circuit(phi): qml.IsingZZ(phi, wires=[0, 1]) return qml.expval(qml.PauliX(0)) - phi = tf.Variable(phi, dtype=tf.complex128) + phi = tf.Variable(phi, dtype=tf.float64) expected = (1 / norm**2) * (-2 * (psi_0 * psi_2 + psi_1 * psi_3) * np.sin(phi)) From 836932cbf0ab18c911195744437f258d893490c2 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 13 Jul 2022 12:38:10 +0200 Subject: [PATCH 10/16] converged matrices and eigvals for diagonal operations --- pennylane/ops/qubit/parametric_ops.py | 126 +++++++++++++++++--------- 1 file changed, 81 insertions(+), 45 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 7f928d76a9d..ee4e7a3fb7d 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -407,13 +407,19 @@ def compute_matrix(phi): # pylint: disable=arguments-differ [0.0000+0.0000j, 0.9689+0.2474j]]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) + p = qml.math.exp(1j * qml.math.cast_like(phi, 1j)) + ones = qml.math.ones_like(p) + zeros = qml.math.zeros_like(p) + + return qml.math.stack([stack_last([ones, zeros]), stack_last([zeros, p])], axis=-2) + signs = qml.math.array([0, 1], like=phi) arg = 1j * phi + if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp([0, arg])) + return qml.math.diag(qml.math.exp(arg * signs)) - diags = qml.math.exp(stack_last([qml.math.zeros_like(arg), arg])) + diags = qml.math.exp(qml.math.outer(arg, signs)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) @staticmethod @@ -444,13 +450,15 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ tensor([1.0000+0.0000j, 0.8776+0.4794j]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) + phase = qml.math.exp(1j * qml.math.cast_like(phi, 1j)) + return stack_last([qml.math.ones_like(phase), phase]) - arg = 1j * phi + prefactors = qml.math.array([0, 1j], like=phi) if qml.math.ndim(phi) == 0: - return qml.math.stack([1, qml.math.exp(arg)]) - - return qml.math.exp(stack_last([qml.math.zeros_like(arg), arg])) + product = phi * prefactors + else: + product = qml.math.outer(phi, prefactors) + return qml.math.exp(product) @staticmethod def compute_decomposition(phi, wires): @@ -545,7 +553,7 @@ def compute_matrix(phi): # pylint: disable=arguments-differ The canonical matrix is the textbook matrix representation that does not consider wires. Implicitly, this assumes that the wires of the operator correspond to the global wire order. - .. seealso:: :meth:`~.PhaseShift.matrix` + .. seealso:: :meth:`~.ControlledPhaseShift.matrix` Args: phi (tensor_like or float): phase shift @@ -555,21 +563,28 @@ def compute_matrix(phi): # pylint: disable=arguments-differ **Example** - >>> qml.PhaseShift.compute_matrix(torch.tensor(0.5)) + >>> qml.ControlledPhaseShift.compute_matrix(torch.tensor(0.5)) tensor([[1.0+0.0j, 0.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 1.0+0.0j, 0.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 0.0+0.0j, 1.0+0.0j, 0.0000+0.0000j], [0.0+0.0j, 0.0+0.0j, 0.0+0.0j, 0.8776+0.4794j]]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) + p = qml.math.exp(1j * qml.math.cast_like(phi, 1j)) + if qml.math.ndim(p) == 0: + return qml.math.diag([1, 1, 1, p]) - phase = qml.math.exp(1j * phi) - if qml.math.ndim(phase) == 0: - return qml.math.diag([1, 1, 1, phase]) + ones = qml.math.ones_like(p) + diags = stack_last([ones, ones, ones, p]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) - ones = qml.math.ones_like(phase) - diags = stack_last([ones, ones, ones, phase]) + signs = qml.math.array([0, 0, 0, 1], like=phi) + arg = 1j * phi + + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * signs)) + + diags = qml.math.exp(qml.math.outer(arg, signs)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod @@ -600,14 +615,16 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ tensor([1.0000+0.0000j, 1.0000+0.0000j, 1.0000+0.0000j, 0.8776+0.4794j]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) - - phase = qml.math.exp(1j * phi) - if qml.math.ndim(phase) == 0: - return qml.math.stack([1, 1, 1, phase]) + phase = qml.math.exp(1j * qml.math.cast_like(phi, 1j)) + ones = qml.math.ones_like(phase) + return stack_last([ones, ones, ones, phase]) - ones = qml.math.ones_like(phase) - return stack_last([ones, ones, ones, phase]) + prefactors = qml.math.array([0, 0, 0, 1j], like=phi) + if qml.math.ndim(phi) == 0: + product = phi * prefactors + else: + product = qml.math.outer(phi, prefactors) + return qml.math.exp(product) @staticmethod def compute_decomposition(phi, wires): @@ -1670,14 +1687,21 @@ def compute_matrix(theta): # pylint: disable=arguments-differ [0.0+0.0j, 0.0+0.0j, 0.0+0.0j, 0.9689+0.2474j]]) """ if qml.math.get_interface(theta) == "tensorflow": - theta = qml.math.cast_like(theta, 1j) + p = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) + if qml.math.ndim(p) == 0: + return qml.math.diag([1, 1, p, qml.math.conj(p)]) - phase = qml.math.exp(0.5j * theta) - if qml.math.ndim(theta) == 0: - return qml.math.diag([1, 1, qml.math.conj(phase), phase]) + ones = qml.math.ones_like(p) + diags = stack_last([ones, ones, p, qml.math.conj(p)]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) - ones = qml.math.ones_like(phase) - diags = stack_last([ones, ones, qml.math.conj(phase), phase]) + signs = qml.math.array([0, 0, 1, -1], like=theta) + arg = -0.5j * theta + + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * signs)) + + diags = qml.math.exp(qml.math.outer(arg, signs)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod @@ -1708,14 +1732,16 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ tensor([1.0000+0.0000j, 1.0000+0.0000j, 0.9689-0.2474j, 0.9689+0.2474j]) """ if qml.math.get_interface(theta) == "tensorflow": - theta = qml.math.cast_like(theta, 1j) + phase = qml.math.exp(-0.5j * qml.math.cast_like(theta, 1j)) + ones = qml.math.ones_like(phase) + return stack_last([ones, ones, phase, qml.math.conj(phase)]) - phase = qml.math.exp(0.5j * theta) + prefactors = qml.math.array([0, 0, -0.5j, 0.5j], like=theta) if qml.math.ndim(theta) == 0: - return qml.math.stack([1, 1, qml.math.conj(phase), phase]) - - ones = qml.math.zeros_like(phase) - return stack_last([ones, ones, qml.math.conj(phase), phase]) + product = theta * prefactors + else: + product = qml.math.outer(theta, prefactors) + return qml.math.exp(product) @staticmethod def compute_decomposition(phi, wires): @@ -2667,14 +2693,20 @@ def compute_matrix(phi): # pylint: disable=arguments-differ [0.0000+0.0000j, 0.0000+0.0000j, 0.0000+0.0000j, 0.9689-0.2474j]]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) + p = qml.math.exp(-0.5j * qml.math.cast_like(phi, 1j)) + if qml.math.ndim(p) == 0: + return qml.math.diag([p, qml.math.conj(p), qml.math.conj(p), p]) - phase = qml.math.exp(0.5j * phi) - phase_conj = qml.math.conj(phase) - if qml.math.ndim(phi) == 0: - return qml.math.diag([phase_conj, phase, phase, phase_conj]) + diags = stack_last([p, qml.math.conj(p), qml.math.conj(p), p]) + return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) + + signs = qml.math.array([1, -1, -1, 1], like=phi) + arg = -0.5j * phi + + if qml.math.ndim(arg) == 0: + return qml.math.diag(qml.math.exp(arg * signs)) - diags = stack_last([phase_conj, phase, phase, phase_conj]) + diags = qml.math.exp(qml.math.outer(arg, signs)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(4, like=diags), diags) @staticmethod @@ -2705,11 +2737,15 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ tensor([0.9689-0.2474j, 0.9689+0.2474j, 0.9689+0.2474j, 0.9689-0.2474j]) """ if qml.math.get_interface(phi) == "tensorflow": - phi = qml.math.cast_like(phi, 1j) + phase = qml.math.exp(-0.5j * qml.math.cast_like(phi, 1j)) + return stack_last([phase, qml.math.conj(phase), qml.math.conj(phase), phase]) - phase = qml.math.exp(0.5j * phi) - phase_conj = qml.math.conj(phase) - return stack_last([phase_conj, phase, phase, phase_conj]) + prefactors = qml.math.array([-0.5j, 0.5j, 0.5j, -0.5j], like=phi) + if qml.math.ndim(phi) == 0: + product = phi * prefactors + else: + product = qml.math.outer(phi, prefactors) + return qml.math.exp(product) def adjoint(self): (phi,) = self.parameters From a161a5c5e57eb2c2e9daac6f0ff7f5c4f61a6f05 Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 13 Jul 2022 13:04:56 +0200 Subject: [PATCH 11/16] compatibility --- pennylane/ops/qubit/parametric_ops.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index ee4e7a3fb7d..0ec80b634e8 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2414,18 +2414,20 @@ def compute_matrix(phi): # pylint: disable=arguments-differ c = qml.math.cos(phi / 2) s = qml.math.sin(phi / 2) + eye = qml.math.eye(4, like=phi) + rev_eye = 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) + eye = qml.math.cast_like(eye, 1j) + rev_eye = qml.math.cast_like(rev_eye, 1j) # The following avoids casting an imaginary quantity to reals when backpropagating js = -1j * s if qml.math.ndim(phi) == 0: - return c * np.eye(4) + js * np.eye(4)[::-1] + return c * eye + js * rev_eye - return qml.math.tensordot(c, np.eye(4), axes=0) + qml.math.tensordot( - js, np.eye(4)[::-1], axes=0 - ) + return qml.math.tensordot(c, eye, axes=0) + qml.math.tensordot(js, rev_eye, axes=0) @staticmethod def compute_decomposition(phi, wires): From 28de4c0bbf43e621652ef8cc36160d6d26db415b Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 13 Jul 2022 14:23:41 +0200 Subject: [PATCH 12/16] test coverage --- tests/ops/qubit/test_parametric_ops.py | 213 ++++++++++++++++++++++++- 1 file changed, 212 insertions(+), 1 deletion(-) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index 0c4c2911953..d154db1b330 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -927,6 +927,33 @@ def get_expected(theta): qml.IsingXY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 ) + def test_isingxy_broadcasted(self, tol): + """Test that the broadcasted IsingXY operation is correct""" + z = np.zeros(3) + assert np.allclose(qml.IsingXY.compute_matrix(z), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.IsingXY(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + + def get_expected(theta): + expected = np.array([np.eye(4, dtype=np.complex128) for i in theta], dtype=complex) + sin_coeff = 1j * np.sin(theta / 2) + expected[:, 1, 1] = np.cos(theta / 2) + expected[:, 2, 2] = np.cos(theta / 2) + expected[:, 1, 2] = 1j * np.sin(theta / 2) + expected[:, 2, 1] = 1j * np.sin(theta / 2) + return expected + + param = np.array([np.pi / 2, np.pi]) + assert np.allclose(qml.IsingXY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingXY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + param = np.array([2.152, np.pi / 2, 0.213]) + assert np.allclose(qml.IsingXY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingXY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) def test_isingxy_eigvals(self, phi, tol): """Test eigenvalues computation for IsingXY""" @@ -939,6 +966,15 @@ def test_isingxy_eigvals(self, phi, tol): ] assert qml.math.allclose(evs, evs_expected) + def test_isingxy_eigvals_broadcasted(self, tol): + """Test broadcasted eigenvalues computation for IsingXY""" + phi = np.linspace(-np.pi, np.pi, 10) + evs = qml.IsingXY.compute_eigvals(phi) + evs_expected = np.array( + [[qml.math.exp(1j * _phi / 2), qml.math.exp(-1j * _phi / 2), 1, 1] for _phi in phi] + ) + assert qml.math.allclose(evs, evs_expected) + @pytest.mark.tf @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) def test_isingxy_eigvals_tf(self, phi, tol): @@ -1163,7 +1199,9 @@ def get_expected(theta): param = np.array([np.pi, 0.1242]) param_tf = tf.Variable(param) - assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + 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""" @@ -1319,6 +1357,43 @@ def test_CRZ(self, tol): 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) + @pytest.mark.tf + def test_CRZ_tf(self, tol): + """Test controlled z rotation is correct when used with Tensorflow, + because the code differs in that case.""" + import tensorflow as tf + + # test identity for theta=0 + z = tf.Variable(0.0) + assert np.allclose(qml.CRZ.compute_matrix(z), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.CRZ(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + + # test identity for theta=pi/2 + expected_pi_half = np.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-1j * np.pi / 4), 0], + [0, 0, 0, np.exp(1j * np.pi / 4)], + ] + ) + phi = tf.Variable(np.pi / 2) + assert np.allclose(qml.CRZ.compute_matrix(phi), expected_pi_half, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(phi, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0) + + # test identity for theta=pi + expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]]) + phi = tf.Variable(np.pi) + assert np.allclose(qml.CRZ.compute_matrix(phi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(phi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + expected = [expected_pi_half, expected_pi] + param_tf = tf.Variable(param) + assert np.allclose(qml.CRZ.compute_matrix(param_tf), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(param_tf, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + def test_CRot(self, tol): """Test controlled arbitrary rotation is correct""" @@ -1493,6 +1568,23 @@ def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op): res = op.eigvals() assert np.allclose(np.diag(res), exp) + @pytest.mark.tf + @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5]) + @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) + def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op): + """Tests that the ControlledPhaseShift and CPhase operation calculate the correct + matrix and eigenvalues for the Tensorflow interface, because the code differs + in that case.""" + import tensorflow as tf + + op = cphase_op(tf.Variable(phi), wires=[0, 1]) + res = op.matrix() + exp = ControlledPhaseShift(phi) + assert np.allclose(res, exp) + + res = op.eigvals() + assert np.allclose(np.diag(res), exp) + @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) def test_controlled_phase_shift_matrix_and_eigvals_broadcasted(self, cphase_op): """Tests that the ControlledPhaseShift and CPhase operation calculate the @@ -1509,6 +1601,125 @@ def test_controlled_phase_shift_matrix_and_eigvals_broadcasted(self, cphase_op): exp_eigvals[..., 3] = np.exp(1j * phi) assert np.allclose(res, exp_eigvals) + @pytest.mark.tf + @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) + def test_controlled_phase_shift_matrix_and_eigvals_broadcasted_tf(self, cphase_op): + """Tests that the ControlledPhaseShift and CPhase operation calculate the + correct matrix and eigenvalues for broadcasted parameters and Tensorflow, + because the code differs for that interface.""" + import tensorflow as tf + + phi = np.array([0.2, np.pi / 2, -0.1]) + phi_tf = tf.Variable(phi) + op = cphase_op(phi_tf, wires=[0, 1]) + res = op.matrix() + expected = np.array([np.eye(4, dtype=complex)] * 3) + expected[..., 3, 3] = np.exp(1j * phi) + assert np.allclose(res, expected) + + res = op.eigvals() + exp_eigvals = np.ones((3, 4), dtype=complex) + exp_eigvals[..., 3] = np.exp(1j * phi) + assert np.allclose(res, exp_eigvals) + + +class TestEigvals: + """Test eigvals of parametrized operations.""" + + def test_rz_eigvals(self, tol): + """Test eigvals of z rotation are correct""" + + # test identity for theta=0 + assert np.allclose(qml.RZ.compute_eigvals(0), np.ones(2), atol=tol, rtol=0) + assert np.allclose(qml.RZ(0, wires=0).eigvals(), np.ones(2), atol=tol, rtol=0) + + # test identity for theta=pi/2 + expected = np.exp([-1j * np.pi / 4, 1j * np.pi / 4]) + assert np.allclose(qml.RZ.compute_eigvals(np.pi / 2), expected, atol=tol, rtol=0) + assert np.allclose(qml.RZ(np.pi / 2, wires=0).eigvals(), expected, atol=tol, rtol=0) + + # test identity for broadcasted theta=pi/2 + expected = np.tensordot([1, 1], np.exp([-1j * np.pi / 4, 1j * np.pi / 4]), axes=0) + pi_half = np.array([np.pi / 2, np.pi / 2]) + assert np.allclose(qml.RZ.compute_eigvals(pi_half), expected, atol=tol, rtol=0) + assert np.allclose(qml.RZ(pi_half, wires=0).eigvals(), expected, atol=tol, rtol=0) + + # test identity for theta=pi + assert np.allclose(qml.RZ.compute_eigvals(np.pi), np.diag(-1j * Z), atol=tol, rtol=0) + assert np.allclose(qml.RZ(np.pi, wires=0).eigvals(), np.diag(-1j * Z), atol=tol, rtol=0) + + def test_phase_shift_eigvals(self, tol): + """Test phase shift eigvals are correct""" + + # test identity for theta=0 + assert np.allclose(qml.PhaseShift.compute_eigvals(0), np.ones(2), atol=tol, rtol=0) + assert np.allclose(qml.PhaseShift(0, wires=0).eigvals(), np.ones(2), atol=tol, rtol=0) + + # test arbitrary phase shift + phi = 0.5432 + expected = np.array([1, np.exp(1j * phi)]) + assert np.allclose(qml.PhaseShift.compute_eigvals(phi), expected, atol=tol, rtol=0) + + # test arbitrary broadcasted phase shift + phi = np.array([0.5, 0.4, 0.3]) + expected = np.array([[1, np.exp(1j * p)] for p in phi]) + assert np.allclose(qml.PhaseShift.compute_eigvals(phi), expected, atol=tol, rtol=0) + + def test_crz_eigvals(self, tol): + """Test controlled z rotation eigvals are correct""" + + # test identity for theta=0 + assert np.allclose(qml.CRZ.compute_eigvals(0), np.ones(4), atol=tol, rtol=0) + assert np.allclose(qml.CRZ(0, wires=[0, 1]).eigvals(), np.ones(4), atol=tol, rtol=0) + + # test identity for theta=pi/2 + expected_pi_half = np.array([1, 1, np.exp(-1j * np.pi / 4), np.exp(1j * np.pi / 4)]) + assert np.allclose(qml.CRZ.compute_eigvals(np.pi / 2), expected_pi_half, atol=tol, rtol=0) + assert np.allclose( + qml.CRZ(np.pi / 2, wires=[0, 1]).eigvals(), expected_pi_half, atol=tol, rtol=0 + ) + + # test identity for theta=pi + expected_pi = np.array([1, 1, -1j, 1j]) + assert np.allclose(qml.CRZ.compute_eigvals(np.pi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(np.pi, wires=[0, 1]).eigvals(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + expected = [expected_pi_half, expected_pi] + assert np.allclose(qml.CRZ.compute_eigvals(param), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(param, wires=[0, 1]).eigvals(), expected, atol=tol, rtol=0) + + @pytest.mark.tf + def test_crz_eigvals_tf(self, tol): + """Test controlled z rotation eigvals are correct with Tensorflow, because the + code differs for that interface.""" + import tensorflow as tf + + # test identity for theta=0 + z = tf.Variable(0) + assert np.allclose(qml.CRZ.compute_eigvals(z), np.ones(4), atol=tol, rtol=0) + assert np.allclose(qml.CRZ(z, wires=[0, 1]).eigvals(), np.ones(4), atol=tol, rtol=0) + + # test identity for theta=pi/2 + expected_pi_half = np.array([1, 1, np.exp(-1j * np.pi / 4), np.exp(1j * np.pi / 4)]) + phi = tf.Variable(np.pi / 2) + assert np.allclose(qml.CRZ.compute_eigvals(phi), expected_pi_half, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(phi, wires=[0, 1]).eigvals(), expected_pi_half, atol=tol, rtol=0) + + # test identity for theta=pi + expected_pi = np.array([1, 1, -1j, 1j]) + phi = tf.Variable(np.pi) + assert np.allclose(qml.CRZ.compute_eigvals(phi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(phi, wires=[0, 1]).eigvals(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + param_tf = tf.Variable(param) + expected = [expected_pi_half, expected_pi] + assert np.allclose(qml.CRZ.compute_eigvals(param_tf), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(param_tf, wires=[0, 1]).eigvals(), expected, atol=tol, rtol=0) + class TestGrad: device_methods = [ From 4a63f79b30ed8fd0283f819d3967c6dc79f28e4d Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 14 Jul 2022 14:06:32 +0200 Subject: [PATCH 13/16] isingxy registration --- pennylane/ops/qubit/attributes.py | 1 + pennylane/ops/qubit/parametric_ops.py | 4 ++++ tests/ops/qubit/test_attributes.py | 1 + 3 files changed, 6 insertions(+) diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 322b7455ddb..8358b41171e 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -223,6 +223,7 @@ def __contains__(self, obj): "IsingXX", "IsingYY", "IsingZZ", + "IsingXY", ] ) """Attribute: Operations that support parameter broadcasting. diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 0ec80b634e8..fad20e5b836 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2781,6 +2781,7 @@ class IsingXY(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: The XY operator satisfies a four-term parameter-shift rule .. math:: @@ -2805,6 +2806,9 @@ class IsingXY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(0.5, 1.0)] diff --git a/tests/ops/qubit/test_attributes.py b/tests/ops/qubit/test_attributes.py index 857faf2431d..41ee88e8bba 100644 --- a/tests/ops/qubit/test_attributes.py +++ b/tests/ops/qubit/test_attributes.py @@ -100,6 +100,7 @@ def test_tensor_check(self): "IsingXX", "IsingYY", "IsingZZ", + "IsingXY", ] two_scalar_single_wire_ops = [ From 0c3326a8d07cf397203cba6911d6cf0bd826dfec Mon Sep 17 00:00:00 2001 From: dwierichs Date: Thu, 14 Jul 2022 14:07:38 +0200 Subject: [PATCH 14/16] test attrs --- tests/ops/qubit/test_attributes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ops/qubit/test_attributes.py b/tests/ops/qubit/test_attributes.py index 41ee88e8bba..ede1b343d99 100644 --- a/tests/ops/qubit/test_attributes.py +++ b/tests/ops/qubit/test_attributes.py @@ -116,6 +116,8 @@ def test_tensor_check(self): "CRot", ] +# When adding an operation to the following list, you +# actually need to write a new test! separately_tested_ops = [ "QubitUnitary", "ControlledQubitUnitary", @@ -311,7 +313,7 @@ def test_pauli_rot(self, pauli_word, wires): assert qml.math.allclose(mat2, single_mats) @pytest.mark.parametrize("wires", [[0, "4", 1], [1, 5], [7]]) - def test_pauli_rot(self, wires): + def test_multi_rz(self, wires): """Test that MultiRZ, which is marked as supporting parameter broadcasting, actually does support broadcasting.""" par = np.array([0.25, 2.1, -0.42]) From 4436a9fdc67d6aa05c1fcd0206a43ce96fe67cec Mon Sep 17 00:00:00 2001 From: dwierichs Date: Mon, 18 Jul 2022 14:24:20 +0200 Subject: [PATCH 15/16] isingxy: additional tests and compatibility of compute_eigvals --- pennylane/ops/qubit/parametric_ops.py | 5 ++-- tests/ops/qubit/test_parametric_ops.py | 41 +++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index fad20e5b836..68e5d329c06 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2920,10 +2920,11 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) + signs = np.array([1, -1, 0, 0]) if qml.math.ndim(phi) == 0: - return qml.math.exp(0.5j * phi * np.array([1, -1, 0, 0])) + return qml.math.exp(0.5j * phi * signs) - return qml.math.exp(qml.math.outer(0.5j * phi, [1, -1, 0, 0])) + return qml.math.exp(qml.math.tensordot(0.5j * phi, signs, axes=0)) def adjoint(self): (phi,) = self.parameters diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index d154db1b330..29d3b503067 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -934,7 +934,7 @@ def test_isingxy_broadcasted(self, tol): assert np.allclose(qml.IsingXY(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) def get_expected(theta): - expected = np.array([np.eye(4, dtype=np.complex128) for i in theta], dtype=complex) + expected = np.array([np.eye(4) for i in theta], dtype=complex) sin_coeff = 1j * np.sin(theta / 2) expected[:, 1, 1] = np.cos(theta / 2) expected[:, 2, 2] = np.cos(theta / 2) @@ -991,6 +991,19 @@ def test_isingxy_eigvals_tf(self, phi, tol): ] assert qml.math.allclose(evs, evs_expected) + @pytest.mark.tf + def test_isingxy_eigvals_tf_broadcasted(self, tol): + """Test broadcasted eigenvalues computation for IsingXY on TF""" + import tensorflow as tf + + phi = np.linspace(-np.pi, np.pi, 10) + evs = qml.IsingXY.compute_eigvals(tf.Variable(phi)) + c = np.cos(phi / 2) + s = np.sin(phi / 2) + ones = np.ones_like(c) + expected = np.stack([c + 1j * s, c - 1j * s, ones, ones], axis=-1) + assert qml.math.allclose(evs, expected) + @pytest.mark.torch @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) def test_isingxy_eigvals_torch(self, phi, tol): @@ -1007,6 +1020,19 @@ def test_isingxy_eigvals_torch(self, phi, tol): ] assert qml.math.allclose(evs, evs_expected) + @pytest.mark.torch + def test_isingxy_eigvals_torch_broadcasted(self, tol): + """Test broadcasted eigenvalues computation for IsingXY with torch""" + import torch + + phi = np.linspace(-np.pi, np.pi, 10) + evs = qml.IsingXY.compute_eigvals(torch.tensor(phi, requires_grad=True)) + c = np.cos(phi / 2) + s = np.sin(phi / 2) + ones = np.ones_like(c) + expected = np.stack([c + 1j * s, c - 1j * s, ones, ones], axis=-1) + assert qml.math.allclose(evs.detach().numpy(), expected) + @pytest.mark.jax @pytest.mark.parametrize("phi", np.linspace(-np.pi, np.pi, 10)) def test_isingxy_eigvals_jax(self, phi, tol): @@ -1023,6 +1049,19 @@ def test_isingxy_eigvals_jax(self, phi, tol): ] assert qml.math.allclose(evs, evs_expected) + @pytest.mark.jax + def test_isingxy_eigvals_jax_broadcasted(self, tol): + """Test broadcasted eigenvalues computation for IsingXY with jax""" + import jax + + phi = np.linspace(-np.pi, np.pi, 10) + evs = qml.IsingXY.compute_eigvals(jax.numpy.array(phi)) + c = np.cos(phi / 2) + s = np.sin(phi / 2) + ones = np.ones_like(c) + expected = np.stack([c + 1j * s, c - 1j * s, ones, ones], axis=-1) + assert qml.math.allclose(evs, expected) + def test_isingxx_broadcasted(self, tol): """Test that the broadcasted IsingXX operation is correct""" z = np.zeros(3) From 0c49888285a53a0b10e9247d6f10887f5c58cfad Mon Sep 17 00:00:00 2001 From: dwierichs Date: Wed, 27 Jul 2022 18:25:54 +0200 Subject: [PATCH 16/16] fix U1 matrix --- pennylane/ops/qubit/parametric_ops.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index da178e5e410..4b5fcf67ef9 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -2031,12 +2031,15 @@ def compute_matrix(phi): # pylint: disable=arguments-differ """ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) + fac = qml.math.cast_like([0, 1], 1j) + else: + fac = np.array([0, 1]) arg = 1j * phi if qml.math.ndim(arg) == 0: - return qml.math.diag(qml.math.exp(arg * np.array([0, 1]))) + return qml.math.diag(qml.math.exp(arg * fac)) - diags = qml.math.exp(qml.math.outer(arg, [0, 1])) + diags = qml.math.exp(qml.math.outer(arg, fac)) return diags[:, :, np.newaxis] * qml.math.cast_like(qml.math.eye(2, like=diags), diags) @staticmethod