diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index b4e0d8407dc..cd28e98dc2c 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -77,6 +77,21 @@ def __repr__(self) -> str: def name(self) -> str: return "Hadamard" + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "X"}): INV_SQRT2, + qml.pauli.PauliWord({self.wires[0]: "Z"}): INV_SQRT2, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -242,9 +257,17 @@ class PauliX(Observable, Operation): _queue_category = "_ops" + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + {qml.pauli.PauliWord({self.wires[0]: "X"}): 1.0} + ) + return self._pauli_rep_cached + def __init__(self, wires: Optional[WiresLike] = None, id: Optional[str] = None): super().__init__(wires=wires, id=id) - self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "X"}): 1.0}) + self._pauli_rep_cached = None def label( self, @@ -434,9 +457,17 @@ class PauliY(Observable, Operation): _queue_category = "_ops" + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + {qml.pauli.PauliWord({self.wires[0]: "Y"}): 1.0} + ) + return self._pauli_rep_cached + def __init__(self, wires: WiresLike, id: Optional[str] = None): super().__init__(wires=wires, id=id) - self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "Y"}): 1.0}) + self._pauli_rep_cached = None def __repr__(self) -> str: """String representation.""" @@ -623,9 +654,17 @@ class PauliZ(Observable, Operation): _queue_category = "_ops" + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + {qml.pauli.PauliWord({self.wires[0]: "Z"}): 1.0} + ) + return self._pauli_rep_cached + def __init__(self, wires: WiresLike, id: Optional[str] = None): super().__init__(wires=wires, id=id) - self._pauli_rep = qml.pauli.PauliSentence({qml.pauli.PauliWord({self.wires[0]: "Z"}): 1.0}) + self._pauli_rep_cached = None def __repr__(self) -> str: """String representation.""" @@ -815,6 +854,21 @@ class S(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): 0.5 + 0.5j, + qml.pauli.PauliWord({self.wires[0]: "Z"}): 0.5 - 0.5j, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -927,6 +981,21 @@ class T(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): (0.5 + 0.5 * INV_SQRT2 * (1 + 1.0j)), + qml.pauli.PauliWord({self.wires[0]: "Z"}): (0.5 - 0.5 * INV_SQRT2 * (1 + 1.0j)), + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -1037,6 +1106,21 @@ class SX(Operation): basis = "X" + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): (0.5 + 0.5j), + qml.pauli.PauliWord({self.wires[0]: "X"}): (0.5 - 0.5j), + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -1152,6 +1236,23 @@ class SWAP(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I", self.wires[1]: "I"}): 0.5, + qml.pauli.PauliWord({self.wires[0]: "X", self.wires[1]: "X"}): 0.5, + qml.pauli.PauliWord({self.wires[0]: "Y", self.wires[1]: "Y"}): 0.5, + qml.pauli.PauliWord({self.wires[0]: "Z", self.wires[1]: "Z"}): 0.5, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -1243,6 +1344,21 @@ class ECR(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "X", self.wires[1]: "I"}): INV_SQRT2, + qml.pauli.PauliWord({self.wires[0]: "Y", self.wires[1]: "X"}): -INV_SQRT2, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ r"""Representation of the operator as a canonical matrix in the computational basis (static method). @@ -1371,6 +1487,23 @@ class ISWAP(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I", self.wires[1]: "I"}): 0.5, + qml.pauli.PauliWord({self.wires[0]: "X", self.wires[1]: "X"}): 0.5j, + qml.pauli.PauliWord({self.wires[0]: "Y", self.wires[1]: "Y"}): 0.5j, + qml.pauli.PauliWord({self.wires[0]: "Z", self.wires[1]: "Z"}): 0.5, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ @@ -1488,6 +1621,25 @@ class SISWAP(Operation): batch_size = None + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I", self.wires[1]: "I"}): 0.5 + + 0.5 * INV_SQRT2, + qml.pauli.PauliWord({self.wires[0]: "X", self.wires[1]: "X"}): 0.5j * INV_SQRT2, + qml.pauli.PauliWord({self.wires[0]: "Y", self.wires[1]: "Y"}): 0.5j * INV_SQRT2, + qml.pauli.PauliWord({self.wires[0]: "Z", self.wires[1]: "Z"}): 0.5 + - 0.5 * INV_SQRT2, + } + ) + return self._pauli_rep_cached + + def __init__(self, wires: WiresLike, id: Optional[str] = None): + super().__init__(wires=wires, id=id) + self._pauli_rep_cached = None + @staticmethod @lru_cache() def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index fe02238bafd..f87173ba968 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -31,6 +31,8 @@ stack_last = functools.partial(qml.math.stack, axis=-1) +INV_SQRT2 = 1 / qml.math.sqrt(2) + def _can_replace(x, y): """ @@ -74,11 +76,24 @@ class RX(Operation): grad_method = "A" parameter_frequencies = [(1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): qml.math.cos(self._phi / 2), + qml.pauli.PauliWord({self.wires[0]: "X"}): -1j * qml.math.sin(self._phi / 2), + } + ) + return self._pauli_rep_cached + def generator(self) -> "qml.Hamiltonian": return qml.Hamiltonian([-0.5], [PauliX(wires=self.wires)]) def __init__(self, phi: TensorLike, wires: WiresLike, id: Optional[str] = None): super().__init__(phi, wires=wires, id=id) + self._phi = phi + self._pauli_rep_cached = None @staticmethod def compute_matrix(theta: TensorLike) -> TensorLike: # pylint: disable=arguments-differ @@ -170,11 +185,24 @@ class RY(Operation): grad_method = "A" parameter_frequencies = [(1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): qml.math.cos(self._phi / 2), + qml.pauli.PauliWord({self.wires[0]: "Y"}): -1j * qml.math.sin(self._phi / 2), + } + ) + return self._pauli_rep_cached + def generator(self) -> "qml.Hamiltonian": return qml.Hamiltonian([-0.5], [PauliY(wires=self.wires)]) def __init__(self, phi: TensorLike, wires: WiresLike, id: Optional[str] = None): super().__init__(phi, wires=wires, id=id) + self._phi = phi + self._pauli_rep_cached = None @staticmethod def compute_matrix(theta: TensorLike) -> TensorLike: # pylint: disable=arguments-differ @@ -265,11 +293,24 @@ class RZ(Operation): grad_method = "A" parameter_frequencies = [(1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): qml.math.cos(self._phi / 2), + qml.pauli.PauliWord({self.wires[0]: "Z"}): -1j * qml.math.sin(self._phi / 2), + } + ) + return self._pauli_rep_cached + def generator(self) -> "qml.Hamiltonian": return qml.Hamiltonian([-0.5], [PauliZ(wires=self.wires)]) def __init__(self, phi: TensorLike, wires: WiresLike, id: Optional[str] = None): super().__init__(phi, wires=wires, id=id) + self._phi = phi + self._pauli_rep_cached = None @staticmethod def compute_matrix(theta: TensorLike) -> TensorLike: # pylint: disable=arguments-differ @@ -401,11 +442,26 @@ class PhaseShift(Operation): grad_method = "A" parameter_frequencies = [(1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): 0.5 + * (1 + qml.math.exp(1j * self._phi)), + qml.pauli.PauliWord({self.wires[0]: "Z"}): 0.5 + * (1 - qml.math.exp(1j * self._phi)), + } + ) + return self._pauli_rep_cached + def generator(self) -> "qml.Projector": return qml.Projector(np.array([1]), wires=self.wires) def __init__(self, phi: TensorLike, wires: WiresLike, id: Optional[str] = None): super().__init__(phi, wires=wires, id=id) + self._phi = phi + self._pauli_rep_cached = None def label( self, @@ -580,6 +636,26 @@ class Rot(Operation): grad_method = "A" parameter_frequencies = [(1,), (1,), (1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): qml.math.cos(self._theta / 2) + * qml.math.cos((self._phi + self._omega) / 2), + qml.pauli.PauliWord({self.wires[0]: "X"}): -1j + * qml.math.sin(self._theta / 2) + * qml.math.sin((self._phi - self._omega) / 2), + qml.pauli.PauliWord({self.wires[0]: "Y"}): -1j + * qml.math.cos((self._phi - self._omega) / 2) + * qml.math.sin((self._theta) / 2), + qml.pauli.PauliWord({self.wires[0]: "Z"}): -1j + * qml.math.cos(self._theta / 2) + * qml.math.sin((self._phi + self._omega) / 2), + } + ) + return self._pauli_rep_cached + def __init__( self, phi: TensorLike, @@ -589,6 +665,10 @@ def __init__( id: Optional[str] = None, ): super().__init__(phi, theta, omega, wires=wires, id=id) + self._phi = phi + self._theta = theta + self._omega = omega + self._pauli_rep_cached = None @staticmethod def compute_matrix( @@ -756,11 +836,26 @@ class U1(Operation): grad_method = "A" parameter_frequencies = [(1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): 0.5 + * (1 + qml.math.exp(1j * self._phi)), + qml.pauli.PauliWord({self.wires[0]: "Z"}): 0.5 + * (1 - qml.math.exp(1j * self._phi)), + } + ) + return self._pauli_rep_cached + def generator(self) -> "qml.Projector": return qml.Projector(np.array([1]), wires=self.wires) def __init__(self, phi: TensorLike, wires: WiresLike, id: Optional[str] = None): super().__init__(phi, wires=wires, id=id) + self._phi = phi + self._pauli_rep_cached = None @staticmethod def compute_matrix(phi: TensorLike) -> TensorLike: # pylint: disable=arguments-differ @@ -884,10 +979,34 @@ class U2(Operation): grad_method = "A" parameter_frequencies = [(1,), (1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): 0.5 + * INV_SQRT2 + * (1 + qml.math.exp(1j * (self._delta + self._phi))), + qml.pauli.PauliWord({self.wires[0]: "X"}): 0.5 + * INV_SQRT2 + * (qml.math.exp(1j * self._phi) - qml.math.exp(1j * self._delta)), + qml.pauli.PauliWord({self.wires[0]: "Y"}): -0.5j + * INV_SQRT2 + * (qml.math.exp(1j * self._phi) + qml.math.exp(1j * self._delta)), + qml.pauli.PauliWord({self.wires[0]: "Z"}): 0.5 + * INV_SQRT2 + * (1 - qml.math.exp(1j * (self._delta + self._phi))), + } + ) + return self._pauli_rep_cached + def __init__( self, phi: TensorLike, delta: TensorLike, wires: WiresLike, id: Optional[str] = None ): super().__init__(phi, delta, wires=wires, id=id) + self._phi = phi + self._delta = delta + self._pauli_rep_cached = None @staticmethod def compute_matrix( @@ -1031,6 +1150,27 @@ class U3(Operation): grad_method = "A" parameter_frequencies = [(1,), (1,), (1,)] + @property + def pauli_rep(self): + if self._pauli_rep_cached is None: + self._pauli_rep_cached = qml.pauli.PauliSentence( + { + qml.pauli.PauliWord({self.wires[0]: "I"}): 0.5 + * (1 + qml.math.exp(1j * (self._delta + self._phi))) + * qml.math.cos(self._theta / 2), + qml.pauli.PauliWord({self.wires[0]: "X"}): -0.5 + * (qml.math.exp(1j * self._delta) - qml.math.exp(1j * self._phi)) + * qml.math.sin(self._theta / 2), + qml.pauli.PauliWord({self.wires[0]: "Y"}): -0.5j + * (qml.math.exp(1j * self._delta) + qml.math.exp(1j * self._phi)) + * qml.math.sin(self._theta / 2), + qml.pauli.PauliWord({self.wires[0]: "Z"}): 0.5 + * (1 - qml.math.exp(1j * (self._delta + self._phi))) + * qml.math.cos(self._theta / 2), + } + ) + return self._pauli_rep_cached + def __init__( self, theta: TensorLike, @@ -1040,6 +1180,10 @@ def __init__( id: Optional[str] = None, ): super().__init__(theta, phi, delta, wires=wires, id=id) + self._theta = theta + self._phi = phi + self._delta = delta + self._pauli_rep_cached = None @staticmethod def compute_matrix( diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py index 6fcfe6ff36a..78b52be3fc7 100644 --- a/pennylane/pauli/pauli_arithmetic.py +++ b/pennylane/pauli/pauli_arithmetic.py @@ -983,16 +983,21 @@ def _sum_same_structure_pws_dense(self, pauli_words, wire_order): ml_interface = qml.math.get_interface(coeff) if ml_interface == "torch": data0 = qml.math.convert_like(data0, coeff) - data = coeff * data0 + data = coeff * data0 if np.isscalar(coeff) else np.outer(coeff, data0) + for pw in pauli_words[1:]: coeff = self[pw] csr_data = pw._get_csr_data(wire_order, 1) ml_interface = qml.math.get_interface(coeff) if ml_interface == "torch": csr_data = qml.math.convert_like(csr_data, coeff) - data += self[pw] * csr_data + data += coeff * csr_data if np.isscalar(coeff) else np.outer(self[pw], csr_data) - return qml.math.einsum("ij,i->ij", base_matrix, data) + return ( + qml.math.einsum("ij,i->ij", base_matrix, data) + if np.isscalar(coeff) + else qml.math.einsum("ij,ki->kij", base_matrix, data) + ) def _sum_same_structure_pws(self, pauli_words, wire_order): """Sums Pauli words with the same sparse structure.""" diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index c82b6dba8f4..aed92ad5c5c 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -31,6 +31,7 @@ ISWAP, SISWAP, SWAP, + SX, H, I, S, @@ -66,6 +67,20 @@ (qml.CCZ, CCZ), ] +NON_PARAMETRIZED_OPERATIONS_WITH_PAULI_REP_ALREADY_IMPLEMENTED = [ + (qml.X(0), X), + (qml.Y(0), Y), + (qml.Z(0), Z), + (qml.H(0), H), + (qml.S(0), S), + (qml.T(0, 1), T), + (qml.SX(0), SX), + (qml.SWAP([0, 1]), SWAP), + (qml.ISWAP([0, 1]), ISWAP), + (qml.ECR([0, 1]), ECR), + (qml.SISWAP([0, 1]), SISWAP), +] + STRING_REPR = ( (qml.Identity(0), "I(0)"), (qml.Hadamard(0), "H(0)"), @@ -1313,3 +1328,23 @@ def test_hadamard_alias(self): assert isinstance( qml.H(0), qml.Hadamard ), "qml.H(0) should create an instance of qml.Hadamard" + + +class TestPauliRep: + def test_matrix_and_pauli_rep_equivalence_for_all_non_parametric_ops(self): + """Compares the matrix representation obtained after using the .pauli_rep attribute with the result of the .matrix() method.""" + import re + + for op, _ in NON_PARAMETRIZED_OPERATIONS_WITH_PAULI_REP_ALREADY_IMPLEMENTED: + dim = op.matrix().shape[0] + if dim == 2: + id_str = "I(0)" + if dim == 4: + id_str = "I(0)@I(1)" + op_in_pauli_decomp = str(op.pauli_rep).replace("\n", " ").replace("I", id_str) + for pauli_op in ["I", "X", "Y", "Z"]: + op_in_pauli_decomp = re.sub( + rf"{pauli_op}", "qml." + rf"{pauli_op}", op_in_pauli_decomp + ) + op_in_pauli_decomp = eval(op_in_pauli_decomp).matrix() + assert np.allclose(op.matrix(), op_in_pauli_decomp) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index f9fe0e6d9ab..6da731caff4 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -68,6 +68,17 @@ qml.GlobalPhase(0.123), ] +SINGLE_QUBIT_PARAMETRIZED_OPERATIONS = [ + qml.RX(0.123, wires=0), + qml.RY(1.434, wires=0), + qml.RZ(2.774, wires=0), + qml.Rot(0.123, 0.456, 0.789, wires=0), + qml.PhaseShift(2.133, wires=0), + qml.U1(0.123, wires=0), + qml.U2(3.556, 2.134, wires=0), + qml.U3(2.009, 1.894, 0.7789, wires=0), +] + BROADCASTED_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), @@ -4049,3 +4060,23 @@ def test_op_aliases_are_valid(): """Tests that ops in new files can still be accessed from the old parametric_ops module.""" assert qml.ops.qubit.parametric_ops_multi_qubit.MultiRZ is old_loc_MultiRZ assert qml.ops.qubit.parametric_ops_single_qubit.RX is old_loc_RX + + +class TestPauliRep: + def test_matrix_and_pauli_rep_equivalence_for_all_single_qubit_parametric_ops(self): + """Compares the matrix representation obtained after using the .pauli_rep attribute with the result of the .matrix() method.""" + import re + + for op in SINGLE_QUBIT_PARAMETRIZED_OPERATIONS: + dim = op.matrix().shape[0] + if dim == 2: + id_str = "I(0)" + if dim == 4: + id_str = "I(0)@I(1)" + op_in_pauli_decomp = str(op.pauli_rep).replace("\n", " ").replace("I", id_str) + for pauli_op in ["I", "X", "Y", "Z"]: + op_in_pauli_decomp = re.sub( + rf"{pauli_op}", "qml." + rf"{pauli_op}", op_in_pauli_decomp + ) + op_in_pauli_decomp = eval(op_in_pauli_decomp).matrix() + assert np.allclose(op.matrix(), op_in_pauli_decomp)