Skip to content

Adding Kraus to channel #1071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions toqito/channel_ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from toqito.channel_ops.dual_channel import dual_channel
from toqito.channel_ops.complementary_channel import complementary_channel
from toqito.channel_ops.natural_representation import natural_representation
from toqito.channel_ops.kraus_to_channel import kraus_to_channel
54 changes: 54 additions & 0 deletions toqito/channel_ops/kraus_to_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Converts Kraus operators into the corressponding quantum channel (i.e. superoperator)."""

import numpy as np

from toqito.matrix_ops import tensor


def kraus_to_channel(
kraus_list: list[tuple[np.ndarray, np.ndarray]]
) -> np.ndarray:
r"""Convert a collection of Kraus operators into the corresponding quantum channel (superoperator).

(Section: Kraus Representations of :cite:`Watrous_2018_TQI`).

This function computes the superoperator representation of a quantum channel from its Kraus representation.
Given a list of Kraus operators \(\{A_i, B_i\}\), the superoperator \(\mathcal{E}\) is computed as:

\[
\mathcal{E}(\rho) = \sum_i B_i \rho A_i^\dagger
\]

The resulting quantum channel can be applied to density matrices by reshaping them into vectorized form.

Examples
========

Constructing a simple quantum channel from Kraus operators:

>>> import numpy as np
>>> from toqito.channel_ops import kraus_to_channel
>>> kraus_1 = np.array([[1, 0], [0, 0]])
>>> kraus_2 = np.array([[0, 1], [0, 0]])
>>> kraus_list = [(kraus_1, kraus_1), (kraus_2, kraus_2)]
>>> kraus_to_channel(kraus_list)
array([[1, 0, 0, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]])

See Also
========
func:`.choi_to_kraus`, func:`.kraus_to_choi`
Copy link
Collaborator

@purva-thakre purva-thakre Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shivansh20128 something has gone wrong with the formatting for this. I was trying to make these clickable similar to what I did in #1091

image

Each function should be :func:.name_function where .name_function is sandwiched between `

My apologies!


References
==========
.. bibliography::
:filter: docname in docnames

:param kraus_list: List of tuples (A, B) where A and B are Kraus operators as numpy arrays.
:return: The superoperator as a numpy array.

"""
super_op = sum(tensor(B, A.conj()) for A, B in kraus_list)
return super_op
87 changes: 87 additions & 0 deletions toqito/channel_ops/tests/test_kraus_to_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Tests for kraus to channel."""

import numpy as np
import pytest

import toqito.state_ops
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a unit test where the superoperator acts on a state?

See Section 2.2 of https://arxiv.org/pdf/1509.02921 for more info.

It would also be pretty easy to construct a superoperator from the channels in Section 2.1.1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before going into that, can you tell me if the function has been implemented correctly. Because I was not very sure about it. If you read the previous discussion on the issue, you will see that the two results, one from using the function on a vector, and other from directly applying a series of kraus operators were coming out to be transpose of each other.
To tackle that, I modified the implementation of the function a little by changing the order of A and B ( from (A, B.conj()) to (B, A.conj())) But I am not sure about it.

super_op = sum(tensor(B, A.conj()) for A, B in kraus_list)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Shivansh20128 ,

Thanks for your response. From what I can tell (and what I recall) I believe your approach here is sensible.

Perhaps one way to sanity check your approach would be to convert a set of Kraus operators to a quantum channel. Then, if you can use some of the existing functionality in toqito to check that you can either go from the channel back to the operators, or apply the channel in a way that gives the same result as a quantum channel that was arrived at through another means, perhaps that would be serve as some good sanity checks?

I understand that some of the above was a bit vague, but in general, do you think that's a sensible approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply the channel in a way that gives the same result as a quantum channel that was arrived at through another means, perhaps that would be serve as some good sanity checks?

I tried this approach here:

import numpy as np

from toqito.matrix_ops import tensor


def kraus_to_channel(
    kraus_list: list[tuple[np.ndarray, np.ndarray]]
) -> np.ndarray:
    """Convert a collection of Kraus operators into the corresponding quantum channel (superoperator).

    :param kraus_list: List of tuples (A, B) where A and B are Kraus operators as numpy arrays
    :return: The superoperator as a numpy array
    """
    super_op = sum(tensor(B, A.conj()) for A, B in kraus_list)
    return super_op


from toqito.channel_ops import apply_channel

kraus_operators = [
    (np.array([[1, 0], [0, 0]]), np.array([[1, 0], [0, 0]])),  # Projection onto |0⟩
    (np.array([[0, 1], [0, 0]]), np.array([[0, 1], [0, 0]])),  # Projection onto |1⟩
]

# Generate the quantum channel using your function
quantum_channel = kraus_to_channel(kraus_operators)

# Define a test quantum state (density matrix)
rho = np.array([[0.5, 0.5], [0.5, 0.5]])

# Apply the quantum channel using Toqito's apply_channel function
rho_after_channel = apply_channel(rho, kraus_operators)

# Apply the superoperator to the vectorized form of rho
rho_vec = rho.flatten("F")  # Column-major order
rho_after_super_op = quantum_channel @ rho_vec
rho_after_super_op = rho_after_super_op.reshape(2, 2, order="F")  # Reshape back

# Compare both methods
print("Using apply_channel:\n", rho_after_channel)
print("Using superoperator:\n", rho_after_super_op)
print("Difference:\n", rho_after_channel - rho_after_super_op)

# The difference should be close to zero
assert np.allclose(rho_after_channel, rho_after_super_op), "Mismatch in quantum channel application!"

And I am getting a match on the results:

Using apply_channel:
 [[1. 0.]
 [0. 0.]]
Using superoperator:
 [[1. 0.]
 [0. 0.]]
Difference:
 [[0. 0.]
 [0. 0.]]

If you also think this is good enough, we can go ahead.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to try other values of rho, but I think this is definitely the correct sanity check approach for what you are working on, and it seems right to me! Nice approach, @Shivansh20128 !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I will add more values of rho in the test cases.

from toqito.channel_ops import apply_channel, kraus_to_channel

dim = 2**2
kraus_list = [np.random.randint(-1, 4, (2, dim, dim)) for _ in range(12)]

vector = np.random.randint(-3, 3, (dim, 1))
dm = toqito.matrix_ops.to_density_matrix(vector)
vec_dm = toqito.matrix_ops.vec(dm)

# Random quantum test states (density matrices)
rho_1 = np.array([[0.5, 0.5], [0.5, 0.5]])
rho_2 = np.array([[0.5, 0], [0, 0.5]])
rho_3 = np.array([[1, 0], [0, 0]])
rho_4 = np.array([[0, 0], [0, 1]])

A = np.random.rand(2, 2) + 1j * np.random.rand(2, 2)
rho_5 = A @ A.conj().T
rho_5 /= np.trace(rho_5) # Normalize trace to 1

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have quite a few functions to generate random things. See if those are useful, instead of manually defining your own randomly generated input.

https://toqito--1071.org.readthedocs.build/en/1071/autoapi/rand/index.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on this later.
Thanks

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think specifically in this case the random_density_matrix function would be useful!


p_1 = 0.1 # Probability of bit flip (Bit-Flip Channel)
kraus_operators_1 = [
(np.sqrt(1 - p_1) * np.array([[1, 0], [0, 1]]), np.sqrt(1 - p_1) * np.array([[1, 0], [0, 1]])), # Identity
(np.sqrt(p_1) * np.array([[0, 1], [1, 0]]), np.sqrt(p_1) * np.array([[0, 1], [1, 0]])), # Bit-flip (X)
]

p_2 = 0.2 # Depolarizing Channel
kraus_operators_2 = [
(np.sqrt(1 - 3 * p_2 / 4) * np.eye(2), np.sqrt(1 - 3 * p_2 / 4) * np.eye(2)), # Identity
(np.sqrt(p_2 / 4) * np.array([[0, 1], [1, 0]]), np.sqrt(p_2 / 4) * np.array([[0, 1], [1, 0]])), # X
(np.sqrt(p_2 / 4) * np.array([[0, -1j], [1j, 0]]), np.sqrt(p_2 / 4) * np.array([[0, -1j], [1j, 0]])), # Y
(np.sqrt(p_2 / 4) * np.array([[1, 0], [0, -1]]), np.sqrt(p_2 / 4) * np.array([[1, 0], [0, -1]])), # Z
]

kraus_operators_3 = [
(np.array([[1, 0], [0, 0]]), np.array([[1, 0], [0, 0]])), # Projection onto |0⟩
(np.array([[0, 1], [0, 0]]), np.array([[0, 1], [0, 0]])), # Projection onto |1⟩
]
Comment on lines +27 to +44
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have commonly predefined channels in toqito/channels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on this later.
Thanks


@pytest.mark.parametrize(
"kraus_list",
[
(kraus_list)
],
)
def test_kraus_to_channel(kraus_list):
"""Test kraus_tochannel works as expected for valid inputs."""
calculated = kraus_to_channel(kraus_list)

value = sum(A @ dm @ B.conj().T for A, B in kraus_list)

assert toqito.matrix_ops.unvec(calculated @ vec_dm).all() == value.all()


@pytest.mark.parametrize(
"rho, kraus_operators",
[
(rho_1, kraus_operators_1), (rho_2, kraus_operators_1), (rho_3, kraus_operators_2),
(rho_4, kraus_operators_2), (rho_5, kraus_operators_3), (rho_1, kraus_operators_3)
],
)
def test_kraus_to_channel_on_quantumStates(rho, kraus_operators):
"""Test kraus_to_channel works as expected for valid inputs."""
# Generate the quantum channel using your function
quantum_channel = kraus_to_channel(kraus_operators)

# Apply the quantum channel using Toqito's apply_channel function
rho_after_channel = apply_channel(rho, kraus_operators)

# Apply the superoperator to the vectorized form of rho
rho_vec = rho.flatten("F") # Column-major order
rho_after_super_op = quantum_channel @ rho_vec
rho_after_super_op = rho_after_super_op.reshape(2, 2, order="F") # Reshape back

# Compare both methods
print("Using apply_channel:\n", rho_after_channel)
print("Using superoperator:\n", rho_after_super_op)
print("Difference:\n", rho_after_channel - rho_after_super_op)

# The difference should be close to zero
assert np.allclose(rho_after_channel, rho_after_super_op)
Loading