Repository: quantumlib/Cirq

Issue:
DepolarizingChannel with more than one qubit is buggy
**Description of the issue**

The `cirq.DepolarizingChannel` class is buggy for `n_qubits` larger than 1.

**How to reproduce the issue**

```python
import cirq
cirq.DepolarizingChannel(p=0.1, n_qubits=2)(*cirq.LineQubit.range(2))
```

Gives:
```
ValueError: Wrong number of qubits for <cirq.depolarize(p=0.1,n_qubits=2)>. Expected 1 qubits but got <[cirq.LineQubit(0), cirq.LineQubit(1)]>.
```

Full log:
<details>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-5bc7460d03b5> in <module>
      1 import cirq
      2 
----> 3 cirq.DepolarizingChannel(p=0.1, n_qubits=2)(*cirq.LineQubit.range(2))

~/anaconda3/lib/python3.7/site-packages/cirq/ops/raw_types.py in __call__(self, *args, **kwargs)
    268 
    269     def __call__(self, *args, **kwargs):
--> 270         return self.on(*args, **kwargs)
    271 
    272     def with_probability(self, probability: 'cirq.TParamVal') -> 'cirq.Gate':

~/anaconda3/lib/python3.7/site-packages/cirq/ops/raw_types.py in on(self, *qubits)
    210         from cirq.ops import gate_operation
    211 
--> 212         return gate_operation.GateOperation(self, list(qubits))
    213 
    214     def wrap_in_linear_combination(

~/anaconda3/lib/python3.7/site-packages/cirq/ops/gate_operation.py in __init__(self, gate, qubits)
     58             qubits: The qubits to operate on.
     59         """
---> 60         gate.validate_args(qubits)
     61         self._gate = gate
     62         self._qubits = tuple(qubits)

~/anaconda3/lib/python3.7/site-packages/cirq/ops/raw_types.py in validate_args(self, qubits)
    199             ValueError: The gate can't be applied to the qubits.
    200         """
--> 201         _validate_qid_shape(self, qubits)
    202 
    203     def on(self, *qubits: Qid) -> 'Operation':

~/anaconda3/lib/python3.7/site-packages/cirq/ops/raw_types.py in _validate_qid_shape(val, qubits)
    728         raise ValueError(
    729             'Wrong number of qubits for <{!r}>. '
--> 730             'Expected {} qubits but got <{!r}>.'.format(val, len(qid_shape), qubits)
    731         )
    732     if any(qid.dimension != dimension for qid, dimension in zip(qubits, qid_shape)):

ValueError: Wrong number of qubits for <cirq.depolarize(p=0.1,n_qubits=2)>. Expected 1 qubits but got <[cirq.LineQubit(0), cirq.LineQubit(1)]>.

</details>

**Cirq version**
`'0.10.0.dev'`

**Temporary workaround**
For any users interested in multi-qubit depolarizing noise, this is a possible workaround to avoid the problem:

```python
import itertools
from cirq import AsymmetricDepolarizingChannel

def my_depolarizing_channel(p: float, n_qubits: int):
    """Builds a depolarizing channel from cirq.AsymmetricDepolarizingChannel."""

    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
    return AsymmetricDepolarizingChannel(error_probabilities=error_probabilities)
```


Discussion:
Given that `DepolarizingChannel` inherits from `SingleQubitGate`, it really shouldn't have an `n_qubits` field - users should instead invoke `on_each` to apply the channel to multiple qubits:
```
import cirq
cirq.DepolarizingChannel(p=0.1).on_each(*cirq.LineQubit.range(2))
```
We should remove this parameter. As far as I can see, nothing in Cirq actually consumes it (aside from the string-representation methods for the class), but it's still part of the public API.
Thanks, I see!

Note however that the tensor product of **_n_** local depolarizing channels (obtainable via `on_each`) is not equal to a global **_n_**-qubit depolarizing channel.

As far as I understand, there isn't a direct way to get the latter apart from using `AsymmetricDepolarizingChannel`.



The `AsymmetricDepolarizingChannel` is also single qubit though. I think we need a new concept for this, or generalize the DepolarizingChannel correctly. 
Discussed on Cirq Cynque: @tonybruguier please have a look at this, thank you!
Ack

Resolving patch:
diff --git a/cirq/ops/common_channels.py b/cirq/ops/common_channels.py
--- a/cirq/ops/common_channels.py
+++ b/cirq/ops/common_channels.py
@@ -15,7 +15,7 @@
 """Quantum channels that are commonly used in the literature."""
 
 import itertools
-from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union, TYPE_CHECKING
+from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
 
 import numpy as np
 
@@ -230,8 +230,8 @@ def asymmetric_depolarize(
 
 
 @value.value_equality
-class DepolarizingChannel(gate_features.SingleQubitGate):
-    """A channel that depolarizes a qubit."""
+class DepolarizingChannel(gate_features.SupportsOnEachGate, raw_types.Gate):
+    """A channel that depolarizes one or several qubits."""
 
     def __init__(self, p: float, n_qubits: int = 1) -> None:
         r"""The symmetric depolarizing channel.
@@ -278,6 +278,9 @@ def __init__(self, p: float, n_qubits: int = 1) -> None:
 
         self._delegate = AsymmetricDepolarizingChannel(error_probabilities=error_probabilities)
 
+    def _qid_shape_(self):
+        return (2,) * self._n_qubits
+
     def _mixture_(self) -> Sequence[Tuple[float, np.ndarray]]:
         return self._delegate._mixture_()
 
diff --git a/cirq/ops/gate_features.py b/cirq/ops/gate_features.py
--- a/cirq/ops/gate_features.py
+++ b/cirq/ops/gate_features.py
@@ -31,11 +31,8 @@ def qubit_index_to_equivalence_group_key(self, index: int) -> int:
         return 0
 
 
-class SingleQubitGate(raw_types.Gate, metaclass=abc.ABCMeta):
-    """A gate that must be applied to exactly one qubit."""
-
-    def _num_qubits_(self) -> int:
-        return 1
+class SupportsOnEachGate(raw_types.Gate, metaclass=abc.ABCMeta):
+    """A gate that can be applied to exactly one qubit."""
 
     def on_each(self, *targets: Union[raw_types.Qid, Iterable[Any]]) -> List[raw_types.Operation]:
         """Returns a list of operations applying the gate to all targets.
@@ -48,7 +45,10 @@ def on_each(self, *targets: Union[raw_types.Qid, Iterable[Any]]) -> List[raw_typ
 
         Raises:
             ValueError if targets are not instances of Qid or List[Qid].
+            ValueError if the gate operates on two or more Qids.
         """
+        if self._num_qubits_() > 1:
+            raise ValueError('This gate only supports on_each when it is a one qubit gate.')
         operations = []  # type: List[raw_types.Operation]
         for target in targets:
             if isinstance(target, raw_types.Qid):
@@ -62,6 +62,13 @@ def on_each(self, *targets: Union[raw_types.Qid, Iterable[Any]]) -> List[raw_typ
         return operations
 
 
+class SingleQubitGate(SupportsOnEachGate, metaclass=abc.ABCMeta):
+    """A gate that must be applied to exactly one qubit."""
+
+    def _num_qubits_(self) -> int:
+        return 1
+
+
 class TwoQubitGate(raw_types.Gate, metaclass=abc.ABCMeta):
     """A gate that must be applied to exactly two qubits."""
 
diff --git a/cirq/ops/identity.py b/cirq/ops/identity.py
--- a/cirq/ops/identity.py
+++ b/cirq/ops/identity.py
@@ -13,21 +13,21 @@
 # limitations under the License.
 """IdentityGate."""
 
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, TYPE_CHECKING
+from typing import Any, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING
 
 import numpy as np
 import sympy
 
 from cirq import protocols, value
 from cirq._doc import document
-from cirq.ops import raw_types
+from cirq.ops import gate_features, raw_types
 
 if TYPE_CHECKING:
     import cirq
 
 
 @value.value_equality
-class IdentityGate(raw_types.Gate):
+class IdentityGate(gate_features.SupportsOnEachGate, raw_types.Gate):
     """A Gate that perform no operation on qubits.
 
     The unitary matrix of this gate is a diagonal matrix with all 1s on the
@@ -64,35 +64,6 @@ def _qid_shape_(self) -> Tuple[int, ...]:
     def num_qubits(self) -> int:
         return len(self._qid_shape)
 
-    def on_each(self, *targets: Union['cirq.Qid', Iterable[Any]]) -> List['cirq.Operation']:
-        """Returns a list of operations that applies the single qubit identity
-        to each of the targets.
-
-        Args:
-            *targets: The qubits to apply this gate to.
-
-        Returns:
-            Operations applying this gate to the target qubits.
-
-        Raises:
-            ValueError if targets are not instances of Qid or List[Qid] or
-            the gate from which this is applied is not a single qubit identity
-            gate.
-        """
-        if len(self._qid_shape) != 1:
-            raise ValueError('IdentityGate only supports on_each when it is a one qubit gate.')
-        operations: List['cirq.Operation'] = []
-        for target in targets:
-            if isinstance(target, raw_types.Qid):
-                operations.append(self.on(target))
-            elif isinstance(target, Iterable) and not isinstance(target, str):
-                operations.extend(self.on_each(*target))
-            else:
-                raise ValueError(
-                    'Gate was called with type different than Qid. Type: {}'.format(type(target))
-                )
-        return operations
-
     def __pow__(self, power: Any) -> Any:
         if isinstance(power, (int, float, complex, sympy.Basic)):
             return self


Test patch:
diff --git a/cirq/ops/common_channels_test.py b/cirq/ops/common_channels_test.py
--- a/cirq/ops/common_channels_test.py
+++ b/cirq/ops/common_channels_test.py
@@ -213,6 +213,36 @@ 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_deprecated_on_each_for_depolarizing_channel_one_qubit():
+    q0 = cirq.LineQubit.range(1)
+    op = cirq.DepolarizingChannel(p=0.1, n_qubits=1)
+
+    op.on_each(q0)
+    op.on_each([q0])
+    with pytest.raises(ValueError, match="Gate was called with type different than Qid"):
+        op.on_each('bogus object')
+
+
+def test_deprecated_on_each_for_depolarizing_channel_two_qubits():
+    q0, q1 = cirq.LineQubit.range(2)
+    op = cirq.DepolarizingChannel(p=0.1, n_qubits=2)
+
+    with pytest.raises(ValueError, match="one qubit"):
+        op.on_each(q0, q1)
+
+
+def test_depolarizing_channel_apply_two_qubits():
+    q0, q1 = cirq.LineQubit.range(2)
+    op = cirq.DepolarizingChannel(p=0.1, n_qubits=2)
+    op(q0, q1)
+
+
+def test_asymmetric_depolarizing_channel_apply_two_qubits():
+    q0, q1 = cirq.LineQubit.range(2)
+    op = cirq.AsymmetricDepolarizingChannel(error_probabilities={'XX': 0.1})
+    op(q0, q1)
+
+
 def test_depolarizing_channel_eq():
     et = cirq.testing.EqualsTester()
     c = cirq.depolarize(0.0)
diff --git a/cirq/ops/pauli_string_test.py b/cirq/ops/pauli_string_test.py
--- a/cirq/ops/pauli_string_test.py
+++ b/cirq/ops/pauli_string_test.py
@@ -674,7 +674,10 @@ def test_pass_operations_over_single(shift: int, sign: int):
 def test_pass_operations_over_double(shift: int, t_or_f1: bool, t_or_f2: bool, neg: bool):
     sign = -1 if neg else +1
     q0, q1, q2 = _make_qubits(3)
-    X, Y, Z = (cirq.Pauli.by_relative_index(pauli, shift) for pauli in (cirq.X, cirq.Y, cirq.Z))
+    X, Y, Z = (
+        cirq.Pauli.by_relative_index(cast(cirq.Pauli, pauli), shift)
+        for pauli in (cirq.X, cirq.Y, cirq.Z)
+    )
 
     op0 = cirq.PauliInteractionGate(Z, t_or_f1, X, t_or_f2)(q0, q1)
     ps_before = cirq.PauliString(qubit_pauli_map={q0: Z, q2: Y}, coefficient=sign)