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))