Skip to content

Commit

Permalink
Add symmetric depol (#3361)
Browse files Browse the repository at this point in the history
For issue #3220
  • Loading branch information
tonybruguier committed Sep 28, 2020
1 parent 730cc24 commit fa99585
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 43 deletions.
99 changes: 70 additions & 29 deletions cirq/ops/common_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Quantum channels that are commonly used in the literature."""

import itertools
from typing import (Any, Dict, Iterable, Optional, Sequence, Tuple, Union,
TYPE_CHECKING)

Expand Down Expand Up @@ -77,8 +78,13 @@ def __init__(self,
raise ValueError(f"{k} must have {num_qubits} Pauli gates.")
for k, v in error_probabilities.items():
value.validate_probability(v, f"p({k})")
value.validate_probability(sum(error_probabilities.values()),
'sum(error_probabilities)')
sum_probs = sum(error_probabilities.values())
# TODO(tonybruguier): Instead of forcing the probabilities to add up
# to 1, check whether the identity is missing, and if that is the
# case, automatically add it with the missing probability mass.
if abs(sum_probs - 1.0) > 1e-6:
raise ValueError(
f"Probabilities do not add up to 1 but to {sum_probs}")
self._num_qubits = num_qubits
self._error_probabilities = error_probabilities
else:
Expand Down Expand Up @@ -227,33 +233,53 @@ def asymmetric_depolarize(p_x: Optional[float] = None,
class DepolarizingChannel(gate_features.SingleQubitGate):
"""A channel that depolarizes a qubit."""

def __init__(self, p: float) -> None:
def __init__(self, p: float, n_qubits: int = 1) -> None:
r"""The symmetric depolarizing channel.
This channel applies one of four disjoint possibilities: nothing (the
identity channel) or one of the three pauli gates. The disjoint
probabilities of the three gates are all the same, p / 3, and the
identity is done with probability 1 - p. The supplied probability
must be a valid probability or else this constructor will raise a
ValueError.
This channel applies one of 4**n disjoint possibilities: nothing (the
identity channel) or one of the 4**n - 1 pauli gates. The disjoint
probabilities of the non-identity Pauli gates are all the same,
p / (4**n - 1), and the identity is done with probability 1 - p. The
supplied probability must be a valid probability or else this
constructor will raise a ValueError.
This channel evolves a density matrix via
$$
\rho \rightarrow (1 - p) \rho
+ (p / 3) X \rho X + (p / 3) Y \rho Y + (p / 3) Z \rho Z
+ 1 / (4**n - 1) \sum _i P_i X P_i
$$
where P_i are the 4**n - 1 Pauli gates (excluding the identity).
Args:
p: The probability that one of the Pauli gates is applied. Each of
the Pauli gates is applied independently with probability p / 3.
the Pauli gates is applied independently with probability
p / (4**n - 1).
n_qubits: the number of qubits.
Raises:
ValueError: if p is not a valid probability.
"""

error_probabilities = {}

p_depol = p / (4**n_qubits - 1)
p_identity = 1.0 - p
for pauli_tuple in itertools.product(['I', 'X', 'Y', 'Z'],
repeat=n_qubits):
pauli_string = ''.join(pauli_tuple)
if pauli_string == 'I' * n_qubits:
error_probabilities[pauli_string] = p_identity
else:
error_probabilities[pauli_string] = p_depol

self._p = p
self._delegate = AsymmetricDepolarizingChannel(p / 3, p / 3, p / 3)
self._n_qubits = n_qubits

self._delegate = AsymmetricDepolarizingChannel(
error_probabilities=error_probabilities)

def _mixture_(self) -> Sequence[Tuple[float, np.ndarray]]:
return self._delegate._mixture_()
Expand All @@ -265,55 +291,70 @@ def _value_equality_values_(self):
return self._p

def __repr__(self) -> str:
return 'cirq.depolarize(p={!r})'.format(self._p)
if self._n_qubits == 1:
return f"cirq.depolarize(p={self._p})"
return f"cirq.depolarize(p={self._p},n_qubits={self._n_qubits})"

def __str__(self) -> str:
return 'depolarize(p={!r})'.format(self._p)
if self._n_qubits == 1:
return f"depolarize(p={self._p})"
return f"depolarize(p={self._p},n_qubits={self._n_qubits})"

def _circuit_diagram_info_(self,
args: 'protocols.CircuitDiagramInfoArgs') -> str:
if args.precision is not None:
f = '{:.' + str(args.precision) + 'g}'
return 'D({})'.format(f).format(self._p)
return 'D({!r})'.format(self._p)
return f"D({self._p:.{args.precision}g})"
return f"D({self._p})"

@property
def p(self) -> float:
"""The probability that one of the Pauli gates is applied.
Each of the Pauli gates is applied independently with probability p / 3.
Each of the Pauli gates is applied independently with probability
p / (4**n_qubits - 1).
"""
return self._p

@property
def n_qubits(self) -> int:
"""The number of qubits"""
return self._n_qubits

def _json_dict_(self) -> Dict[str, Any]:
return protocols.obj_to_dict_helper(self, ['p'])
if self._n_qubits == 1:
return protocols.obj_to_dict_helper(self, ['p'])
return protocols.obj_to_dict_helper(self, ['p', 'n_qubits'])


def depolarize(p: float) -> DepolarizingChannel:
def depolarize(p: float, n_qubits: int = 1) -> DepolarizingChannel:
r"""Returns a DepolarizingChannel with given probability of error.
This channel applies one of four disjoint possibilities: nothing (the
identity channel) or one of the three pauli gates. The disjoint
probabilities of the three gates are all the same, p / 3, and the
identity is done with probability 1 - p. The supplied probability
must be a valid probability or else this constructor will raise a
ValueError.
This channel applies one of 4**n disjoint possibilities: nothing (the
identity channel) or one of the 4**n - 1 pauli gates. The disjoint
probabilities of the non-identity Pauli gates are all the same,
p / (4**n - 1), and the identity is done with probability 1 - p. The
supplied probability must be a valid probability or else this constructor
will raise a ValueError.
This channel evolves a density matrix via
$$
\rho \rightarrow (1 - p) \rho
+ (p / 3) X \rho X + (p / 3) Y \rho Y + (p / 3) Z \rho Z
+ 1 / (4**n - 1) \sum _i P_i X P_i
$$
where P_i are the 4**n - 1 Pauli gates (excluding the identity).
Args:
p: The probability that one of the Pauli gates is applied. Each of
the Pauli gates is applied independently with probability p / 3.
the Pauli gates is applied independently with probability
p / (4**n - 1).
n_qubits: The number of qubits.
Raises:
ValueError: if p is not a valid probability.
"""
return DepolarizingChannel(p)
return DepolarizingChannel(p, n_qubits)


@value.value_equality
Expand Down
89 changes: 75 additions & 14 deletions cirq/ops/common_channels_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,31 +131,77 @@ def test_asymmetric_depolarizing_channel_text_diagram():

def test_depolarizing_channel():
d = cirq.depolarize(0.3)
np.testing.assert_almost_equal(cirq.channel(d),
(np.sqrt(0.7) * np.eye(2),
np.sqrt(0.1) * X,
np.sqrt(0.1) * Y,
np.sqrt(0.1) * Z))
np.testing.assert_almost_equal(cirq.channel(d), (
np.sqrt(0.7) * np.eye(2),
np.sqrt(0.1) * X,
np.sqrt(0.1) * Y,
np.sqrt(0.1) * Z,
))
assert cirq.has_channel(d)


def test_depolarizing_channel_two_qubits():
d = cirq.depolarize(0.15, n_qubits=2)
np.testing.assert_almost_equal(cirq.channel(d), (
np.sqrt(0.85) * np.eye(4),
np.sqrt(0.01) * np.kron(np.eye(2), X),
np.sqrt(0.01) * np.kron(np.eye(2), Y),
np.sqrt(0.01) * np.kron(np.eye(2), Z),
np.sqrt(0.01) * np.kron(X, np.eye(2)),
np.sqrt(0.01) * np.kron(X, X),
np.sqrt(0.01) * np.kron(X, Y),
np.sqrt(0.01) * np.kron(X, Z),
np.sqrt(0.01) * np.kron(Y, np.eye(2)),
np.sqrt(0.01) * np.kron(Y, X),
np.sqrt(0.01) * np.kron(Y, Y),
np.sqrt(0.01) * np.kron(Y, Z),
np.sqrt(0.01) * np.kron(Z, np.eye(2)),
np.sqrt(0.01) * np.kron(Z, X),
np.sqrt(0.01) * np.kron(Z, Y),
np.sqrt(0.01) * np.kron(Z, Z),
))
assert cirq.has_channel(d)

def test_depolarizing_mixture():
d = cirq.depolarize(0.3)
assert_mixtures_equal(cirq.mixture(d),
((0.7, np.eye(2)),
(0.1, X),
(0.1, Y),
(0.1, Z)))
((0.7, np.eye(2)), (0.1, X), (0.1, Y), (0.1, Z)))
assert cirq.has_mixture(d)


def test_depolarizing_mixture_two_qubits():
d = cirq.depolarize(0.15, n_qubits=2)
assert_mixtures_equal(cirq.mixture(d),
((0.85, np.eye(4)), (0.01, np.kron(np.eye(2), X)),
(0.01, np.kron(np.eye(2), Y)),
(0.01, np.kron(np.eye(2), Z)),
(0.01, np.kron(X, np.eye(2))), (0.01, np.kron(X, X)),
(0.01, np.kron(X, Y)), (0.01, np.kron(X, Z)),
(0.01, np.kron(Y, np.eye(2))), (0.01, np.kron(Y, X)),
(0.01, np.kron(Y, Y)), (0.01, np.kron(Y, Z)),
(0.01, np.kron(Z, np.eye(2))), (0.01, np.kron(Z, X)),
(0.01, np.kron(Z, Y)), (0.01, np.kron(Z, Z))))
assert cirq.has_mixture(d)


def test_depolarizing_channel_repr():
cirq.testing.assert_equivalent_repr(cirq.DepolarizingChannel(0.3))


def test_depolarizing_channel_repr_two_qubits():
cirq.testing.assert_equivalent_repr(
cirq.DepolarizingChannel(0.3, n_qubits=2))


def test_depolarizing_channel_str():
assert str(cirq.depolarize(0.3)) == 'depolarize(p=0.3)'


def test_depolarizing_channel_str_two_qubits():
assert str(cirq.depolarize(0.3,
n_qubits=2)) == 'depolarize(p=0.3,n_qubits=2)'


def test_depolarizing_channel_eq():
et = cirq.testing.EqualsTester()
c = cirq.depolarize(0.0)
Expand All @@ -166,9 +212,9 @@ def test_depolarizing_channel_eq():


def test_depolarizing_channel_invalid_probability():
with pytest.raises(ValueError, match='was less than 0'):
with pytest.raises(ValueError, match=re.escape('p(I) was greater than 1.')):
cirq.depolarize(-0.1)
with pytest.raises(ValueError, match='was greater than 1'):
with pytest.raises(ValueError, match=re.escape('p(I) was less than 0.')):
cirq.depolarize(1.1)


Expand All @@ -180,6 +226,22 @@ def test_depolarizing_channel_text_diagram():
assert (cirq.circuit_diagram_info(
d, args=round_to_2_prec) == cirq.CircuitDiagramInfo(
wire_symbols=('D(0.12)',)))
assert (cirq.circuit_diagram_info(
d, args=no_precision) == cirq.CircuitDiagramInfo(
wire_symbols=('D(0.1234567)',)))


def test_depolarizing_channel_text_diagram_two_qubits():
d = cirq.depolarize(0.1234567, n_qubits=2)
assert (cirq.circuit_diagram_info(
d, args=round_to_6_prec) == cirq.CircuitDiagramInfo(
wire_symbols=('D(0.123457)',)))
assert (cirq.circuit_diagram_info(
d, args=round_to_2_prec) == cirq.CircuitDiagramInfo(
wire_symbols=('D(0.12)',)))
assert (cirq.circuit_diagram_info(
d, args=no_precision) == cirq.CircuitDiagramInfo(
wire_symbols=('D(0.1234567)',)))


def test_generalized_amplitude_damping_channel():
Expand Down Expand Up @@ -539,9 +601,8 @@ def test_bad_error_probabilities_gate():
def test_bad_probs():
with pytest.raises(ValueError, match=re.escape('p(X) was greater than 1.')):
cirq.asymmetric_depolarize(error_probabilities={'X': 1.1, 'Y': -0.1})
with pytest.raises(
ValueError,
match=re.escape('sum(error_probabilities) was greater than 1.')):
with pytest.raises(ValueError,
match=re.escape('Probabilities do not add up to 1')):
cirq.asymmetric_depolarize(error_probabilities={'X': 0.7, 'Y': 0.6})


Expand Down
5 changes: 5 additions & 0 deletions cirq/protocols/json_test_data/MultiDepolarizingChannel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cirq_type": "DepolarizingChannel",
"p": 0.5,
"n_qubits": 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cirq.depolarize(p=0.5,n_qubits=2)

0 comments on commit fa99585

Please sign in to comment.