Skip to content

Commit

Permalink
Added TritFlip (#5784)
Browse files Browse the repository at this point in the history
**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 <bottrill@student.ubc.ca>
Co-authored-by: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 6, 2024
1 parent 4abe8ac commit 7f0c9d6
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 13 deletions.
1 change: 1 addition & 0 deletions doc/introduction/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ Qutrit noisy channels

~pennylane.QutritDepolarizingChannel
~pennylane.QutritAmplitudeDamping
~pennylane.TritFlip

:html:`</div>`

Expand Down
8 changes: 6 additions & 2 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
5 changes: 1 addition & 4 deletions pennylane/ops/qutrit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
"THermitian",
"GellMann",
}
__channels__ = {
"QutritDepolarizingChannel",
"QutritAmplitudeDamping",
}
__channels__ = {"QutritDepolarizingChannel", "QutritAmplitudeDamping", "TritFlip"}

__all__ = list(__ops__ | __obs__ | __channels__)
125 changes: 121 additions & 4 deletions pennylane/ops/qutrit/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <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]
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
109 changes: 106 additions & 3 deletions tests/ops/qutrit/test_qutrit_channel_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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))

0 comments on commit 7f0c9d6

Please sign in to comment.