Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,10 @@
dirac_notation,
eye_tensor,
fidelity,
kraus_to_channel_matrix,
kraus_to_choi,
one_hot,
operation_to_channel_matrix,
operation_to_choi,
QUANTUM_STATE_LIKE,
QuantumState,
Expand Down
2 changes: 2 additions & 0 deletions cirq-core/cirq/qis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"""Tools and methods for quantum information science."""

from cirq.qis.channels import (
kraus_to_channel_matrix,
kraus_to_choi,
operation_to_channel_matrix,
operation_to_choi,
)

Expand Down
26 changes: 26 additions & 0 deletions cirq-core/cirq/qis/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ def kraus_to_choi(kraus_operators: Sequence[np.ndarray]) -> np.ndarray:
return c


def kraus_to_channel_matrix(kraus_operators: Sequence[np.ndarray]) -> np.ndarray:
"""Returns the matrix representation of the linear map with given Kraus operators."""
d_out, d_in = kraus_operators[0].shape
m = np.zeros((d_out * d_out, d_in * d_in), dtype=np.complex128)
for k in kraus_operators:
m += np.kron(k, k.conj())
return m


def operation_to_choi(operation: 'protocols.SupportsChannel') -> np.ndarray:
r"""Returns the unique Choi matrix associated with a superoperator.

Expand All @@ -49,3 +58,20 @@ def operation_to_choi(operation: 'protocols.SupportsChannel') -> np.ndarray:
Choi matrix corresponding to operation.
"""
return kraus_to_choi(protocols.channel(operation))


def operation_to_channel_matrix(operation: 'protocols.SupportsChannel') -> np.ndarray:
"""Returns the matrix representation of a superoperator in standard basis.

Let E: L(H1) -> L(H2) denote a linear map which takes linear operators on Hilbert space H1
to linear operators on Hilbert space H2 and let d1 = dim H1 and d2 = dim H2. Also, let Fij
denote an operator whose matrix has one in ith row and jth column and zeros everywhere else.
Note that d1-by-d1 operators Fij form a basis of L(H1). Similarly, d2-by-d2 operators Fij
form a basis of L(H2). This function returns the matrix of E in these bases.

Args:
operation: Quantum channel.
Returns:
Matrix representation of operation.
"""
return kraus_to_channel_matrix(protocols.channel(operation))
98 changes: 79 additions & 19 deletions cirq-core/cirq/qis/channels_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for channels."""
from typing import Iterable

import numpy as np
import pytest

Expand All @@ -28,32 +30,38 @@ def apply_channel(channel: cirq.SupportsChannel, rho: np.ndarray) -> np.ndarray:
return out


def expected_choi(channel: cirq.SupportsChannel) -> np.ndarray:
def generate_standard_operator_basis(d_out: int, d_in: int) -> Iterable[np.ndarray]:
for i in range(d_out):
for j in range(d_in):
e_ij = np.zeros((d_out, d_in))
e_ij[i, j] = 1
yield e_ij


def compute_choi(channel: cirq.SupportsChannel) -> np.ndarray:
ks = cirq.channel(channel)
d_out, d_in = ks[0].shape
d = d_in * d_out
c = np.zeros((d, d), dtype=np.complex128)
for i in range(d_in):
for j in range(d_in):
e_ij = np.zeros((d_in, d_in))
e_ij[i, j] = 1
c += np.kron(apply_channel(channel, e_ij), e_ij)
for e in generate_standard_operator_basis(d_in, d_in):
c += np.kron(apply_channel(channel, e), e)
return c


def compute_channel_matrix(channel: cirq.SupportsChannel) -> np.ndarray:
ks = cirq.channel(channel)
d_out, d_in = ks[0].shape
m = np.zeros((d_out * d_out, d_in * d_in), dtype=np.complex128)
for k, e_in in enumerate(generate_standard_operator_basis(d_in, d_in)):
m[:, k] = np.reshape(apply_channel(channel, e_in), d_out * d_out)
return m


@pytest.mark.parametrize(
'kraus_operators, expected_choi',
(
([np.eye(2)], np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]])),
(
[
np.eye(2) / 2,
np.array([[0, 1], [1, 0]]) / 2,
np.array([[0, -1j], [1j, 0]]) / 2,
np.diag([1, -1]) / 2,
],
np.eye(4) / 2,
),
(cirq.channel(cirq.depolarize(0.75)), np.eye(4) / 2),
(
[
np.array([[1, 0, 0], [0, 0, 1]]) / np.sqrt(2),
Expand All @@ -80,14 +88,66 @@ def test_kraus_to_choi(kraus_operators, expected_choi):
),
)
def test_operation_to_choi(channel):
"""Verifies that cirq.choi correctly computes the Choi matrix."""
"""Verifies that cirq.operation_to_choi correctly computes the Choi matrix."""
n_qubits = cirq.num_qubits(channel)
actual = cirq.operation_to_choi(channel)
expected = expected_choi(channel)
expected = compute_choi(channel)
assert np.isclose(np.trace(actual), 2 ** n_qubits)
assert np.all(actual == expected)


def test_choi_on_completely_dephasing_channel():
"""Checks that cirq.choi returns the right matrix for the completely dephasing channel."""
def test_choi_for_completely_dephasing_channel():
"""Checks cirq.operation_to_choi on the completely dephasing channel."""
assert np.all(cirq.operation_to_choi(cirq.phase_damp(1)) == np.diag([1, 0, 0, 1]))


@pytest.mark.parametrize(
'kraus_operators, expected_channel_matrix',
(
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like these parameterizations are reused across tests. Could you define constants for them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Most of the repeated constants were the Kraus operators of the completely depolarizing channel. I changed the code to use a short cirq expression to generate those instead of hardcoding. I also changed Kraus operators in another test case. The remaining constants are either different or very short (e.g. np.eye(2)). This cuts away almost all duplication (the Choi and channel matrices were of course different all along) while keeping all inputs local so the reader does not need to scroll to examine correctness.

([np.eye(2)], np.eye(4)),
(
cirq.channel(cirq.depolarize(0.75)),
np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]) / 2,
),
(
[
np.array([[0, 1, 0], [0, 0, 1]]) / np.sqrt(2),
np.array([[0, 1, 0], [0, 0, -1]]) / np.sqrt(2),
],
np.array(
[
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1],
]
),
),
),
)
def test_kraus_to_channel_matrix(kraus_operators, expected_channel_matrix):
Copy link
Collaborator

Choose a reason for hiding this comment

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

To avoid confusion, expected_channel_matrix should not be reused here since it's already the name of a function in this file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed functions to use verbs in the name.

"""Verifies that cirq.kraus_to_channel_matrix computes the correct channel matrix."""
assert np.allclose(cirq.kraus_to_channel_matrix(kraus_operators), expected_channel_matrix)


@pytest.mark.parametrize(
'channel',
(
cirq.I,
cirq.X,
cirq.CNOT,
cirq.depolarize(0.1),
cirq.depolarize(0.1, n_qubits=2),
cirq.amplitude_damp(0.2),
),
)
def test_operation_to_channel_matrix(channel):
"""Verifies that cirq.channel_matrix correctly computes the channel matrix."""
actual = cirq.operation_to_channel_matrix(channel)
expected = compute_channel_matrix(channel)
assert np.all(actual == expected)


def test_channel_matrix_for_completely_dephasing_channel():
"""Checks cirq.operation_to_channel_matrix on the completely dephasing channel."""
assert np.all(cirq.operation_to_channel_matrix(cirq.phase_damp(1)) == np.diag([1, 0, 0, 1]))