Skip to content
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

More channel representation conversion tools #4553

Merged
merged 1 commit into from
Oct 5, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
from cirq.qis import (
bloch_vector_from_state_vector,
choi_to_kraus,
choi_to_superoperator,
CliffordTableau,
density_matrix,
density_matrix_from_state_vector,
Expand All @@ -379,6 +380,8 @@
QuantumState,
quantum_state,
STATE_VECTOR_LIKE,
superoperator_to_choi,
superoperator_to_kraus,
to_valid_density_matrix,
to_valid_state_vector,
validate_density_matrix,
Expand Down
3 changes: 3 additions & 0 deletions cirq-core/cirq/qis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@

from cirq.qis.channels import (
choi_to_kraus,
choi_to_superoperator,
kraus_to_channel_matrix,
kraus_to_choi,
kraus_to_superoperator,
operation_to_channel_matrix,
operation_to_choi,
operation_to_superoperator,
superoperator_to_choi,
superoperator_to_kraus,
)

from cirq.qis.clifford_tableau import CliffordTableau
Expand Down
32 changes: 32 additions & 0 deletions cirq-core/cirq/qis/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,38 @@ def kraus_to_superoperator(kraus_operators: Sequence[np.ndarray]) -> np.ndarray:
return m


def superoperator_to_kraus(superoperator: np.ndarray) -> Sequence[np.ndarray]:
"""Returns a Kraus representation of a channel specified via the superoperator matrix."""
return choi_to_kraus(superoperator_to_choi(superoperator))


def choi_to_superoperator(choi: np.ndarray) -> np.ndarray:
"""Returns the superoperator matrix of a quantum channel specified via the Choi matrix."""
d = int(np.round(np.sqrt(choi.shape[0])))
if choi.shape != (d * d, d * d):
raise ValueError(f"Invalid Choi matrix shape, expected {(d * d, d * d)}, got {choi.shape}")
if not np.allclose(choi, choi.T.conj()):
raise ValueError("Choi matrix must be Hermitian")

c = np.reshape(choi, (d, d, d, d))
s = np.swapaxes(c, 1, 2)
return np.reshape(s, (d * d, d * d))


def superoperator_to_choi(superoperator: np.ndarray) -> np.ndarray:
"""Returns the Choi matrix of a quantum channel specified via the superoperator matrix."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we have documentation anywhere that defines what these representations are? If no: we should beef up these docstrings to give some context for the conversion.

In any case, it could be helpful to give a little note about how "involved" each conversion is. E.g. add a line: "Conversions between superoperator and choi representations only involve axes manipulation"

"Conversions to kraus require an eigendecomposition"

Copy link
Collaborator Author

@viathor viathor Oct 5, 2021

Choose a reason for hiding this comment

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

Good point. I'll send thorough updates to docstrings in a separate PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in #4554 (both definitions of the mathematical objects and the notes about complexity).

d = int(np.round(np.sqrt(superoperator.shape[0])))
if superoperator.shape != (d * d, d * d):
raise ValueError(
f"Invalid superoperator matrix shape, expected {(d * d, d * d)}, "
f"got {superoperator.shape}"
)

s = np.reshape(superoperator, (d, d, d, d))
c = np.swapaxes(s, 1, 2)
return np.reshape(c, (d * d, d * d))


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

Expand Down
180 changes: 180 additions & 0 deletions cirq-core/cirq/qis/channels_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,186 @@ def test_kraus_to_superoperator(kraus_operators, expected_superoperator):
assert np.allclose(cirq.kraus_to_channel_matrix(kraus_operators), expected_superoperator)


@pytest.mark.parametrize(
'superoperator, expected_kraus_operators',
(
(np.eye(4), [np.eye(2)]),
(np.diag([1, -1, -1, 1]), [np.diag([1, -1])]),
(np.diag([1, -1j, 1j, 1]), [np.diag([1, 1j])]),
(
np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]) / 2,
cirq.kraus(cirq.depolarize(0.75)),
),
),
)
def test_superoperator_to_kraus_fixed_values(superoperator, expected_kraus_operators):
"""Verifies that cirq.kraus_to_superoperator computes the correct channel matrix."""
actual_kraus_operators = cirq.superoperator_to_kraus(superoperator)
for i in (0, 1):
for j in (0, 1):
input_rho = np.zeros((2, 2))
input_rho[i, j] = 1
actual_rho = apply_kraus_operators(actual_kraus_operators, input_rho)
expected_rho = apply_kraus_operators(expected_kraus_operators, input_rho)
assert np.allclose(actual_rho, expected_rho)


@pytest.mark.parametrize(
'superoperator',
(
np.eye(4),
np.diag([1, 0, 0, 1]),
np.diag([1, -1j, 1j, 1]),
np.array(
[
[1, 0, 0, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 1],
]
),
np.array(
[
[1, 0, 0, 0.8],
[0, 0.36, 0, 0],
[0, 0, 0.36, 0],
[0, 0, 0, 0.64],
],
),
),
)
def test_superoperator_to_kraus_inverse_of_kraus_to_superoperator(superoperator):
"""Verifies that cirq.kraus_to_superoperator(cirq.superoperator_to_kraus(.)) is identity."""
kraus = cirq.superoperator_to_kraus(superoperator)
recovered_superoperator = cirq.kraus_to_superoperator(kraus)
assert np.allclose(recovered_superoperator, superoperator)


@pytest.mark.parametrize(
'choi, error',
(
(np.array([[1, 2, 3], [4, 5, 6]]), "shape"),
(np.eye(2), "shape"),
(
np.array(
[
[0.6, 0.0, -0.1j, 0.1],
[0.0, 0.0, 0.0, 0.0],
[0.1j, 0.0, 0.4, 0.0],
[0.2, 0.0, 0.0, 1.0],
]
),
"Hermitian",
),
),
)
def test_choi_to_superoperator_invalid_input(choi, error):
with pytest.raises(ValueError, match=error):
_ = cirq.choi_to_superoperator(choi)


@pytest.mark.parametrize(
'superoperator, error',
(
(np.array([[1, 2, 3], [4, 5, 6]]), "shape"),
(np.eye(2), "shape"),
),
)
def test_superoperator_to_choi_invalid_input(superoperator, error):
with pytest.raises(ValueError, match=error):
_ = cirq.superoperator_to_choi(superoperator)


@pytest.mark.parametrize(
'superoperator, choi',
(
(
# Identity channel
np.eye(4),
np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]),
),
(
# S gate
np.diag([1, -1j, 1j, 1]),
np.array([[1, 0, 0, -1j], [0, 0, 0, 0], [0, 0, 0, 0], [1j, 0, 0, 1]]),
),
(
# Hadamard
np.array([[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]]) / 2,
np.array([[1, 1, 1, -1], [1, 1, 1, -1], [1, 1, 1, -1], [-1, -1, -1, 1]]) / 2,
),
(
# Completely dephasing channel
np.diag([1, 0, 0, 1]),
np.diag([1, 0, 0, 1]),
),
(
# Amplitude damping channel
np.array(
[
[1, 0, 0, 0.36],
[0, 0.8, 0, 0],
[0, 0, 0.8, 0],
[0, 0, 0, 0.64],
],
),
np.array(
[
[1, 0, 0, 0.8],
[0, 0.36, 0, 0],
[0, 0, 0, 0],
[0.8, 0, 0, 0.64],
],
),
),
(
# Completely depolarizing channel
np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]) / 2,
np.eye(4) / 2,
),
),
)
def test_superoperator_vs_choi_fixed_values(superoperator, choi):
recovered_choi = cirq.superoperator_to_choi(superoperator)
assert np.allclose(recovered_choi, choi)

recovered_superoperator = cirq.choi_to_superoperator(choi)
assert np.allclose(recovered_superoperator, superoperator)


@pytest.mark.parametrize(
'choi',
(
np.eye(4),
np.diag([1, 0, 0, 1]),
np.diag([0.2, 0.3, 0.8, 0.7]),
np.array(
[
[1, 0, 1, 0],
[0, 1, 0, -1],
[1, 0, 1, 0],
[0, -1, 0, 1],
]
),
np.array(
[
[0.8, 0, 0, 0.5],
[0, 0.3, 0, 0],
[0, 0, 0.2, 0],
[0.5, 0, 0, 0.7],
],
),
),
)
def test_choi_to_superoperator_inverse_of_superoperator_to_choi(choi):
superoperator = cirq.choi_to_superoperator(choi)
recovered_choi = cirq.superoperator_to_choi(superoperator)
assert np.allclose(recovered_choi, choi)

recovered_superoperator = cirq.choi_to_superoperator(recovered_choi)
assert np.allclose(recovered_superoperator, superoperator)


@pytest.mark.parametrize(
'channel',
(
Expand Down