Repository: qiboteam/qibo

Issue:
Usage of Gate.on_qubits() method
Hi! I'm trying to understand how to use the ``qibo.abstractions.abstract_gates.Gate.on_qubits()`` method.
By reading the documentation, I understood that such method is used to obtain a gate that acts on some target qubits by cloning a gate that was originally created with different targets.

Indeed, in the following example it works
```python
from qibo.models import Circuit
from qibo import gates

c = Circuit(2)
gate = gates.H(0)
c.add(gate)
c.add(gate.on_qubits(1))
print(c().state())
```
but in this one it doesn't work
```python
from qibo.models import Circuit
from qibo import gates

c = Circuit(2)
gate = gates.H(1)
c.add(gate)
c.add(gate.on_qubits(0))
print(c().state())
```
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "...qibo/src/qibo/abstractions/abstract_gates.py", line 214, in on_qubits
    gate = self.__class__(*qubits, **self.init_kwargs)
  File "...qibo/src/qibo/abstractions/abstract_gates.py", line 213, in <genexpr>
    qubits = (q[i] for i in self.qubits)
IndexError: tuple index out of range

```
Am I using the method properly?

Discussion:

Patch:
diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml
index 2cee9a68a..d64c103d4 100644
--- a/.github/workflows/rules.yml
+++ b/.github/workflows/rules.yml
@@ -20,7 +20,7 @@ jobs:
       uses: mstksg/get-package@v1
       with:
         brew: libomp
-    - name: Set numba version for py37 on linux and macos
+    - name: Set numba version for macos
       if: startsWith(matrix.os, 'macos')
       run: |
         python -m pip install --upgrade pip
@@ -29,7 +29,6 @@ jobs:
       if: startsWith(matrix.os, 'windows') == 0
       run: |
         python -m pip install --upgrade pip
-        pip install pylint
         pip install pytest-cov
         pip install .[tests]
         pip install qibotf
@@ -38,11 +37,11 @@ jobs:
       if: startsWith(matrix.os, 'windows')
       run: |
         python -m pip install --upgrade pip
-        pip install pylint
         pip install pytest-cov
         pip install .[qibojit,tests]
     - name: Test with pylint
       run: |
+        pip install pylint
         pylint src -E -d E1123,E1120
     - name: Test with pytest core
       run: |
diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py
index 5dc87d375..3bbd349ba 100644
--- a/src/qibo/abstractions/abstract_gates.py
+++ b/src/qibo/abstractions/abstract_gates.py
@@ -91,7 +91,7 @@ class Gate:
 
     @target_qubits.setter
     def target_qubits(self, qubits: Sequence[int]):
-        """Sets control qubits tuple."""
+        """Sets target qubits tuple."""
         self._set_target_qubits(qubits)
         self._check_control_target_overlap()
 
@@ -195,15 +195,36 @@ class Gate:
         b = not (t1 & set(gate.qubits) or t2 & set(self.qubits))
         return a or b
 
-    def on_qubits(self, *q) -> "Gate":
-        """Creates the same gate targeting different qubits.
+    def _on_qubits(self, *q) -> "Gate":
+        """Helper method for :meth:`qibo.abstractions.circuit.AbstractCircuit.on_qubits`.
+
+        Creates the same gate targeting different qubits.
 
         Args:
             q (int): Qubit index (or indeces) that the new gate should act on.
+                Note that q is interpreted as a map from the original qubit ids
+                to the new ones. It is required for `len(q)` to be greater than
+                the max qubit id of the original gate.
 
         Returns:
             A :class:`qibo.abstractions.gates.Gate` object of the original gate
             type targeting the given qubits.
+
+        Example:
+            ::
+
+                from qibo import models, gates
+                c = models.Circuit(4)
+                # Add some CNOT gates
+                c.add(gates.CNOT(2, 3)._on_qubits(0, 1, 2, 3)) # equivalent to gates.CNOT(2, 3)
+                c.add(gates.CNOT(2, 3)._on_qubits(1, 2, 3, 0)) # equivalent to gates.CNOT(3, 0)
+                c.add(gates.CNOT(2, 3)._on_qubits(2, 0, 1, 3)) # equivalent to gates.CNOT(1, 3)
+                c.add(gates.CNOT(2, 3)._on_qubits(0, 3, 2, 1)) # equivalent to gates.CNOT(2, 1)
+                print(c.draw())
+                # q0: ───X─────
+                # q1: ───|─o─X─
+                # q2: ─o─|─|─o─
+                # q3: ─X─o─X───
         """
         if self.is_controlled_by:
             targets = (q[i] for i in self.target_qubits)
@@ -292,7 +313,7 @@ class SpecialGate(Gate):
     def commutes(self, gate):
         return False
 
-    def on_qubits(self, *q):
+    def _on_qubits(self, *q):
         raise_error(NotImplementedError,
                     "Cannot use special gates on subroutines.")
 
@@ -346,10 +367,10 @@ class Channel(Gate):
         """"""
         raise_error(ValueError, "Noise channel cannot be controlled on qubits.")
 
-    def on_qubits(self, *q): # pragma: no cover
+    def _on_qubits(self, *q): # pragma: no cover
         # future TODO
-        raise_error(NotImplementedError, "`on_qubits` method is not available "
-                                         "for the `GeneralChannel` gate.")
+        raise_error(NotImplementedError, "`_on_qubits` method is not available "
+                                         "for the `Channel` gate.")
 
 
 class ParametrizedGate(Gate):
diff --git a/src/qibo/abstractions/circuit.py b/src/qibo/abstractions/circuit.py
index 3138b5e81..29a4588fa 100644
--- a/src/qibo/abstractions/circuit.py
+++ b/src/qibo/abstractions/circuit.py
@@ -178,7 +178,7 @@ class AbstractCircuit(ABC):
                                     "the circuit contains {} qubits."
                                     "".format(len(q), self.nqubits))
         for gate in self.queue:
-            yield gate.on_qubits(*q)
+            yield gate._on_qubits(*q)
 
     def copy(self, deep: bool = False):
         """Creates a copy of the current ``circuit`` as a new ``Circuit`` model.
diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py
index 9430f440c..2fdc011bb 100644
--- a/src/qibo/abstractions/gates.py
+++ b/src/qibo/abstractions/gates.py
@@ -624,7 +624,7 @@ class CZ(Gate):
 
 
 class _CRn_(ParametrizedGate):
-    """Abstract method for defining the CU1, CU2 and CU3 gates.
+    """Abstract method for defining the CRX, CRY and CRZ gates.
 
     Args:
         q0 (int): the control qubit id number.
@@ -1067,7 +1067,7 @@ class Unitary(ParametrizedGate):
     def rank(self) -> int:
         return len(self.target_qubits)
 
-    def on_qubits(self, *q) -> "Gate":
+    def _on_qubits(self, *q) -> "Gate":
         args = [self.init_args[0]]
         args.extend((q[i] for i in self.target_qubits))
         gate = self.__class__(*args, **self.init_kwargs)


Test patch:
diff --git a/src/qibo/tests/test_abstract_gates.py b/src/qibo/tests/test_abstract_gates.py
index 64187c273..ecbd43158 100644
--- a/src/qibo/tests/test_abstract_gates.py
+++ b/src/qibo/tests/test_abstract_gates.py
@@ -211,7 +211,7 @@ def test_flatten():
     gate = gates.Flatten([1, 2, 3, 4])
     assert gate.coefficients == [1, 2, 3, 4]
     with pytest.raises(NotImplementedError):
-        gate.on_qubits(0, 2)
+        gate._on_qubits(0, 2)
 
 
 def test_kraus_channel_init():
@@ -332,7 +332,7 @@ def test_gates_commute():
 
 
 def test_on_qubits():
-    gate = gates.CNOT(0, 1).on_qubits(2, 3)
+    gate = gates.CNOT(0, 1)._on_qubits(2, 3)
     assert gate.target_qubits == (3,)
     assert gate.control_qubits == (2,)
     assert isinstance(gate, gates.CNOT)
@@ -356,7 +356,7 @@ def test_controlled_by():
 
 def test_on_qubits_controlled_by():
     gate = gates.H(0).controlled_by(1, 2)
-    gate = gate.on_qubits(5, 4, 6)
+    gate = gate._on_qubits(5, 4, 6)
     assert gate.target_qubits == (5,)
     assert gate.control_qubits == (4, 6)
     assert isinstance(gate, gates.H)
@@ -374,4 +374,4 @@ def test_special_gate():
     gate = abstract_gates.SpecialGate()
     assert not gate.commutes(gates.H(0))
     with pytest.raises(NotImplementedError):
-        gate.on_qubits(1)
+        gate._on_qubits(1)