Repository: PennyLaneAI/pennylane
Pull request #5784 (2024-06-01 05:42:09)

Added TritFlip
**Context:**
`default.qutrit.mixed` device has been added, but only two channels have been added , this adds the third channel to the device so the device can simulate the qutrit equivalent of bitflip.

**Description of the Change:**
Adds TritFlip channel which allows for "bitflips" between subspaces of the qutrit.

**Benefits:**
Makes it possible to simulate  single state parameter flips.

**Possible Drawbacks:**
The parameter flips are done together, this makes it impossible to write the partial derivatives parameter shift rules generally. Also it is inefficient if you only want to simulate one flip occurring, such as just 1 and 2.

**Related GitHub Issues:**
N/A

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5784?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.66%. Comparing base [(`ec0ebd6`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/ec0ebd608f4c6729340f76b57d902948e51f868a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`1933b76`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/1933b76cd8c6150da920886a2f6c7a3a4e2645b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5784      +/-   ##
==========================================
- Coverage   99.67%   99.66%   -0.01%     
==========================================
  Files         413      413              
  Lines       39250    38977     -273     
==========================================
- Hits        39122    38848     -274     
- Misses        128      129       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5784?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/introduction/operations.rst b/doc/introduction/operations.rst
index b4b9b0d3c..ac7e23efc 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:`</div>`
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 255014a7c..962b9a005 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)
 
 <h3>Breaking changes 💔</h3>
 
diff --git a/pennylane/ops/qutrit/__init__.py b/pennylane/ops/qutrit/__init__.py
index da3ee27ca..7dbc601a6 100644
--- a/pennylane/ops/qutrit/__init__.py
+++ b/pennylane/ops/qutrit/__init__.py
@@ -48,9 +48,6 @@ __obs__ = {
     "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 d124b8f0c..e9f632761 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 @@ class QutritDepolarizingChannel(Channel):
         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 @@ class QutritAmplitudeDamping(Channel):
             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 <https://docs.pennylane.ai/en/stable/code/api/pennylane.BitFlip.html>`_ 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 <https://arxiv.org/abs/2309.11303>`_] (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]


Test patch:
diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_preprocessing.py
index 0d80adf29..c1358cba5 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 @@ class TestPreprocessing:
             (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 e0be689b6..3497c650b 100644
--- a/tests/ops/qutrit/test_qutrit_channel_ops.py
+++ b/tests/ops/qutrit/test_qutrit_channel_ops.py
@@ -135,7 +135,7 @@ class TestQutritDepolarizingChannel:
 
     @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 @@ class TestQutritDepolarizingChannel:
 
     @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 @@ class TestQutritDepolarizingChannel:
 
     @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 @@ class TestQutritAmplitudeDamping:
 
         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))