From 7f0c9d6b10cf0a6e219d7fc1a08f3f5eb305f380 Mon Sep 17 00:00:00 2001 From: Gabriel Bottrill <78718539+Gabriel-Bottrill@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:29:12 -0700 Subject: [PATCH] Added TritFlip (#5784) **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 --------- Co-authored-by: Gabriel Bottrill Co-authored-by: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com> --- doc/introduction/operations.rst | 1 + doc/releases/changelog-dev.md | 8 +- pennylane/ops/qutrit/__init__.py | 5 +- pennylane/ops/qutrit/channel.py | 125 +++++++++++++++++- .../test_qutrit_mixed_preprocessing.py | 1 + tests/ops/qutrit/test_qutrit_channel_ops.py | 109 ++++++++++++++- 6 files changed, 236 insertions(+), 13 deletions(-) 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))