diff --git a/doc/introduction/operations.rst b/doc/introduction/operations.rst index b4b9b0d3c4e..ac7e23efcf3 100644 --- a/doc/introduction/operations.rst +++ b/doc/introduction/operations.rst @@ -508,6 +508,7 @@ Qutrit noisy channels ~pennylane.QutritDepolarizingChannel ~pennylane.QutritAmplitudeDamping + ~pennylane.TritFlip :html:`` diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 255014a7ca7..962b9a0056e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -213,14 +213,18 @@ * Implemented kwargs (`check_interface`, `check_trainability`, `rtol` and `atol`) support in `qml.equal` for the operators `Pow`, `Adjoint`, `Exp`, and `SProd`. [(#5668)](https://github.com/PennyLaneAI/pennylane/issues/5668) - -* ``qml.QutritDepolarizingChannel`` has been added, allowing for depolarizing noise to be simulated on the `default.qutrit.mixed` device. + +* `qml.QutritDepolarizingChannel` has been added, allowing for depolarizing noise to be simulated on the `default.qutrit.mixed` device. [(#5502)](https://github.com/PennyLaneAI/pennylane/pull/5502) * `qml.QutritAmplitudeDamping` channel has been added, allowing for noise processes modelled by amplitude damping to be simulated on the `default.qutrit.mixed` device. [(#5503)](https://github.com/PennyLaneAI/pennylane/pull/5503) [(#5757)](https://github.com/PennyLaneAI/pennylane/pull/5757) [(#5799)](https://github.com/PennyLaneAI/pennylane/pull/5799) + +* `qml.TritFlip` has been added, allowing for trit flip errors, such as misclassification, + to be simulated on the `default.qutrit.mixed` device. + [(#5784)](https://github.com/PennyLaneAI/pennylane/pull/5784)

Breaking changes 💔

diff --git a/pennylane/ops/qutrit/__init__.py b/pennylane/ops/qutrit/__init__.py index da3ee27cacc..7dbc601a62d 100644 --- a/pennylane/ops/qutrit/__init__.py +++ b/pennylane/ops/qutrit/__init__.py @@ -48,9 +48,6 @@ "THermitian", "GellMann", } -__channels__ = { - "QutritDepolarizingChannel", - "QutritAmplitudeDamping", -} +__channels__ = {"QutritDepolarizingChannel", "QutritAmplitudeDamping", "TritFlip"} __all__ = list(__ops__ | __obs__ | __channels__) diff --git a/pennylane/ops/qutrit/channel.py b/pennylane/ops/qutrit/channel.py index d124b8f0c2a..e9f632761d7 100644 --- a/pennylane/ops/qutrit/channel.py +++ b/pennylane/ops/qutrit/channel.py @@ -125,7 +125,7 @@ class QutritDepolarizingChannel(Channel): Args: p (float): Each qutrit Pauli operator is applied with probability :math:`\frac{p}{8}` - wires (Sequence[int] or int): the wire the channel acts on + wires (Sequence[int] or int): The wire the channel acts on id (str or None): String representing the operation (optional) """ @@ -142,7 +142,7 @@ def compute_kraus_matrices(p): # pylint:disable=arguments-differ r"""Kraus matrices representing the qutrit depolarizing channel. Args: - p (float): each qutrit Pauli gate is applied with probability :math:`\frac{p}{8}` + p (float): Each qutrit Pauli gate is applied with probability :math:`\frac{p}{8}` Returns: list (array): list of Kraus matrices @@ -278,8 +278,8 @@ class QutritAmplitudeDamping(Channel): gamma_10 (float): :math:`|1 \rangle \rightarrow |0 \rangle` amplitude damping probability. gamma_20 (float): :math:`|2 \rangle \rightarrow |0 \rangle` amplitude damping probability. gamma_21 (float): :math:`|2 \rangle \rightarrow |1 \rangle` amplitude damping probability. - wires (Sequence[int] or int): the wire the channel acts on - id (str or None): String representing the operation (optional) + wires (Sequence[int] or int): the wire the channel acts on. + id (str or None): String representing the operation (optional). """ num_params = 3 @@ -340,3 +340,120 @@ def compute_kraus_matrices(gamma_10, gamma_20, gamma_21): # pylint:disable=argu math.cast_like(math.array([[0, 0, 0], [0, 0, 1], [0, 0, 0]]), gamma_21), gamma_21 ) return [K0, K1, K2, K3] + + +class TritFlip(Channel): + r""" + Single-qutrit trit flip error channel, used for applying "bit flips" on each qutrit subspace. + + This channel is modelled by the following Kraus matrices: + + .. math:: + K_0 = \sqrt{1-(p_{01} + p_{02} + p_{12})} \begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 + \end{bmatrix} + + .. math:: + K_1 = \sqrt{p_{01}}\begin{bmatrix} + 0 & 1 & 0 \\ + 1 & 0 & 0 \\ + 0 & 0 & 1 + \end{bmatrix}, \quad + K_2 = \sqrt{p_{02}}\begin{bmatrix} + 0 & 0 & 1 \\ + 0 & 1 & 0 \\ + 1 & 0 & 0 + \end{bmatrix}, \quad + K_3 = \sqrt{p_{12}}\begin{bmatrix} + 1 & 0 & 0 \\ + 0 & 0 & 1 \\ + 0 & 1 & 0 + \end{bmatrix} + + where :math:`p_{01}, p_{02}, p_{12} \in [0, 1]` is the probability of a "trit flip" occurring + within subspaces (0,1), (0,2), and (1,2) respectively. + + .. note:: + The Kraus operators :math:`\{K_0, K_1, K_2, K_3\}` are adapted from the + `BitFlip `_ channel's Kraus operators. + + This channel is primarily meant to simulate the misclassification inherent to measurements on some platforms. + An example of a measurement with misclassification can be seen in [`1 `_] (Fig 1a). + + To maintain normalization :math:`p_{01} + p_{02} + p_{12} \leq 1`. + + + **Details:** + + * Number of wires: 1 + * Number of parameters: 3 + + Args: + p_01 (float): The probability that a :math:`|0 \rangle \leftrightarrow |1 \rangle` trit flip error occurs. + p_02 (float): The probability that a :math:`|0 \rangle \leftrightarrow |2 \rangle` trit flip error occurs. + p_12 (float): The probability that a :math:`|1 \rangle \leftrightarrow |2 \rangle` trit flip error occurs. + wires (Sequence[int] or int): The wire the channel acts on + id (str or None): String representing the operation (optional) + """ + + num_params = 3 + num_wires = 1 + grad_method = "F" + + def __init__(self, p_01, p_02, p_12, wires, id=None): + # Verify input + ps = (p_01, p_02, p_12) + for p in ps: + if not math.is_abstract(p) and not 0.0 <= p <= 1.0: + raise ValueError("All probabilities must be in the interval [0,1]") + if not any(math.is_abstract(p) for p in ps): + if not 0.0 <= sum(ps) <= 1.0: + raise ValueError("The sum of probabilities must be in the interval [0,1]") + + super().__init__(p_01, p_02, p_12, wires=wires, id=id) + + @staticmethod + def compute_kraus_matrices(p_01, p_02, p_12): # pylint:disable=arguments-differ + r"""Kraus matrices representing the TritFlip channel. + + Args: + p_01 (float): The probability that a :math:`|0 \rangle \leftrightarrow |1 \rangle` trit flip error occurs. + p_02 (float): The probability that a :math:`|0 \rangle \leftrightarrow |2 \rangle` trit flip error occurs. + p_12 (float): The probability that a :math:`|1 \rangle \leftrightarrow |2 \rangle` trit flip error occurs. + + Returns: + list (array): list of Kraus matrices + + **Example** + + >>> qml.TritFlip.compute_kraus_matrices(0.05, 0.01, 0.10) + [ + array([ [0.91651514, 0. , 0. ], + [0. , 0.91651514, 0. ], + [0. , 0. , 0.91651514]]), + array([ [0. , 0.2236068 , 0. ], + [0.2236068 , 0. , 0. ], + [0. , 0. , 0.2236068]]), + array([ [0. , 0. , 0.1 ], + [0. , 0.1 , 0. ], + [0.1 , 0. , 0. ]]), + array([ [0.31622777, 0. , 0. ], + [0. , 0. , 0.31622777], + [0. , 0.31622777, 0. ]]) + ] + """ + K0 = math.sqrt(1 - (p_01 + p_02 + p_12) + math.eps) * math.convert_like( + math.cast_like(np.eye(3), p_01), p_01 + ) + K1 = math.sqrt(p_01 + math.eps) * math.convert_like( + math.cast_like(math.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]]), p_01), p_01 + ) + K2 = math.sqrt(p_02 + math.eps) * math.convert_like( + math.cast_like(math.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]), p_02), p_02 + ) + K3 = math.sqrt(p_12 + math.eps) * math.convert_like( + math.cast_like(math.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]]), p_12), p_12 + ) + return [K0, K1, K2, K3] diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py index 0d80adf2943..c1358cba569 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py @@ -127,6 +127,7 @@ def test_measurement_is_swapped_out(self, mp_fn, mp_cls, shots): (qml.TRX(1.1, 0), True), (qml.QutritDepolarizingChannel(0.4, 0), True), (qml.QutritAmplitudeDamping(0.1, 0.2, 0.12, 0), True), + (qml.TritFlip(0.4, 0.1, 0.02, 0), True), ], ) def test_accepted_operator(self, op, expected): diff --git a/tests/ops/qutrit/test_qutrit_channel_ops.py b/tests/ops/qutrit/test_qutrit_channel_ops.py index e0be689b688..3497c650b50 100644 --- a/tests/ops/qutrit/test_qutrit_channel_ops.py +++ b/tests/ops/qutrit/test_qutrit_channel_ops.py @@ -135,7 +135,7 @@ def test_kraus_jac_autograd(self): @pytest.mark.torch def test_kraus_jac_torch(self): - """Tests Jacobian of Kraus matrices using torch.""" + """Tests Jacobian of Kraus matrices using PyTorch.""" import torch p = torch.tensor(0.43, requires_grad=True) @@ -145,7 +145,7 @@ def test_kraus_jac_torch(self): @pytest.mark.tf def test_kraus_jac_tf(self): - """Tests Jacobian of Kraus matrices using tensorflow.""" + """Tests Jacobian of Kraus matrices using TensorFlow.""" import tensorflow as tf p = tf.Variable(0.43) @@ -161,7 +161,7 @@ def test_kraus_jac_tf(self): @pytest.mark.jax def test_kraus_jac_jax(self): - """Tests Jacobian of Kraus matrices using jax.""" + """Tests Jacobian of Kraus matrices using JAX.""" import jax jax.config.update("jax_enable_x64", True) @@ -288,3 +288,106 @@ def test_kraus_jac_jax(self): jac = jax.jacobian(self.kraus_fn, argnums=[0, 1, 2])(gamma_10, gamma_20, gamma_21) assert math.allclose(jac, self.expected_jac_fn(gamma_10, gamma_20, gamma_21)) + + +class TestTritFlip: + """Tests for the quantum channel TritFlip""" + + @pytest.mark.parametrize( + "ps", [(0, 0, 0), (0.1, 0.12, 0.3), (0.5, 0.4, 0.1), (1, 0, 0), (0, 1, 0), (0, 0, 1)] + ) + def test_ps_arbitrary(self, ps, tol): + """Test that various values of p give correct Kraus matrices""" + kraus_mats = qml.TritFlip(*ps, wires=0).kraus_matrices() + + expected_K0 = np.sqrt(1 - sum(ps)) * np.eye(3) + assert np.allclose(kraus_mats[0], expected_K0, atol=tol, rtol=0) + + Ks = [ + [[0, 1, 0], [1, 0, 0], [0, 0, 1]], + [[0, 0, 1], [0, 1, 0], [1, 0, 0]], + [[1, 0, 0], [0, 0, 1], [0, 1, 0]], + ] + + for p, K, res in zip(ps, Ks, kraus_mats[1:]): + expected_K = np.sqrt(p) * np.array(K) + assert np.allclose(res, expected_K, atol=tol, rtol=0) + + @pytest.mark.parametrize( + "p_01,p_02,p_12", + [(1.2, 0, 0), (0, -0.3, 0.5), (0, 0, 1 + math.eps), (1, math.eps, 0), (0.3, 0.4, 0.4)], + ) + def test_p_invalid_parameter(self, p_01, p_02, p_12): + """Ensures that error is thrown when p_01, p_02, p_12, or their sum are outside [0,1]""" + with pytest.raises(ValueError, match="must be in the interval"): + qml.TritFlip(p_01, p_02, p_12, wires=0).kraus_matrices() + + @staticmethod + def expected_jac_fn(p_01, p_02, p_12): + """Gets the expected Jacobian of Kraus matrices""" + # Set up the 3 partial derivatives of the 4 3x3 Kraus Matrices + partials = math.zeros((3, 4, 3, 3)) + + # All 3 partials have the same first Kraus Operator output + partials[:, 0] = -1 / (2 * math.sqrt(1 - (p_01 + p_02 + p_12))) * math.eye(3) + + # Set the matrix defined by each partials parameter, the rest are 0 + partials[0, 1] = 1 / (2 * math.sqrt(p_01)) * math.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) + partials[1, 2] = 1 / (2 * math.sqrt(p_02)) * math.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]]) + partials[2, 3] = 1 / (2 * math.sqrt(p_12)) * math.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]]) + return partials + + @staticmethod + def kraus_fn(p_01, p_02, p_12): + """Gets a matrix of the Kraus matrices to be tested.""" + return qml.math.stack(qml.TritFlip(p_01, p_02, p_12, wires=0).kraus_matrices()) + + @pytest.mark.autograd + def test_kraus_jac_autograd(self): + """Tests Jacobian of Kraus matrices using autograd.""" + + p_01 = pnp.array(0.14, requires_grad=True) + p_02 = pnp.array(0.04, requires_grad=True) + p_12 = pnp.array(0.23, requires_grad=True) + jac = qml.jacobian(self.kraus_fn)(p_01, p_02, p_12) + assert qml.math.allclose(jac, self.expected_jac_fn(p_01, p_02, p_12)) + + @pytest.mark.torch + def test_kraus_jac_torch(self): + """Tests Jacobian of Kraus matrices using PyTorch.""" + import torch + + ps = [0.14, 0.04, 0.23] + + p_01 = torch.tensor(ps[0], requires_grad=True) + p_02 = torch.tensor(ps[1], requires_grad=True) + p_12 = torch.tensor(ps[2], requires_grad=True) + + jac = torch.autograd.functional.jacobian(self.kraus_fn, (p_01, p_02, p_12)) + expected_jac = self.expected_jac_fn(*ps) + for j, exp in zip(jac, expected_jac): + assert qml.math.allclose(j.detach().numpy(), exp) + + @pytest.mark.tf + def test_kraus_jac_tf(self): + """Tests Jacobian of Kraus matrices using TensorFlow.""" + import tensorflow as tf + + p_01 = tf.Variable(0.14) + p_02 = tf.Variable(0.04) + p_12 = tf.Variable(0.23) + with tf.GradientTape() as tape: + out = self.kraus_fn(p_01, p_02, p_12) + jac = tape.jacobian(out, (p_01, p_02, p_12)) + assert qml.math.allclose(jac, self.expected_jac_fn(p_01, p_02, p_12)) + + @pytest.mark.jax + def test_kraus_jac_jax(self): + """Tests Jacobian of Kraus matrices using JAX.""" + import jax + + p_01 = jax.numpy.array(0.14) + p_02 = jax.numpy.array(0.04) + p_12 = jax.numpy.array(0.23) + jac = jax.jacobian(self.kraus_fn, argnums=[0, 1, 2])(p_01, p_02, p_12) + assert qml.math.allclose(jac, self.expected_jac_fn(p_01, p_02, p_12))