Repository: qiboteam/qibo

Issue:
Fuse of inverse of multiple gates gives multiple gates 
When building the inverse gate of all gates in a circuit, the gates are not fused but applied one by one, meaning they are not fused.
This is problematic for simulating noise for example.
``` python
from qibo import models, gates
circuit = models.Circuit(1)
circuit.add(gates.H(0))
circuit.add(gates.X(0))
inverse = circuit.invert().fuse().queue[0]
circuit.add(inverse)
print(circuit.draw()) # q0: ─H─X─X─H─
```
Right now, the single fused inverse gate has to be build by hand.
``` python
from qibo import models, gates
circuit = models.Circuit(1)
circuit.add(gates.H(0))
circuit.add(gates.X(0))
inverse = circuit.invert().fuse().queue[0]
circuit.add(gates.Unitary(inverse.matrix,0))
print(circuit.draw()) # q0: ─H─X─U─
```



Discussion:

Patch:
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
index 5c2a7c2c6..eda74c25a 100644
--- a/src/qibo/gates/gates.py
+++ b/src/qibo/gates/gates.py
@@ -4,6 +4,7 @@ from typing import Dict, List, Optional, Tuple
 from qibo.config import raise_error
 from qibo.gates.abstract import Gate, ParametrizedGate
 
+# TODO: Make these dictionaries gate properties
 QASM_GATES = {
     "h": "H",
     "x": "X",
@@ -51,6 +52,53 @@ PARAMETRIZED_GATES = {
     "cu1",
     "cu3",
 }
+DRAW_LABELS = {
+    "h": "H",
+    "x": "X",
+    "y": "Y",
+    "z": "Z",
+    "s": "S",
+    "sdg": "SDG",
+    "t": "T",
+    "tdg": "TDG",
+    "rx": "RX",
+    "ry": "RY",
+    "rz": "RZ",
+    "u1": "U1",
+    "u2": "U2",
+    "u3": "U3",
+    "cx": "X",
+    "swap": "x",
+    "iswap": "i",
+    "cz": "Z",
+    "crx": "RX",
+    "cry": "RY",
+    "crz": "RZ",
+    "cu1": "U1",
+    "cu3": "U3",
+    "ccx": "X",
+    "id": "I",
+    "measure": "M",
+    "fsim": "f",
+    "generalizedfsim": "gf",
+    "rxx": "RXX",
+    "ryy": "RYY",
+    "rzz": "RZZ",
+    "Unitary": "U",
+    "fswap": "fx",
+    "PauliNoiseChannel": "PN",
+    "KrausChannel": "K",
+    "UnitaryChannel": "U",
+    "ThermalRelaxationChannel": "TR",
+    "DepolarizingChannel": "D",
+    "ResetChannel": "R",
+    "PartialTrace": "PT",
+    "EntanglementEntropy": "EE",
+    "Norm": "N",
+    "Overlap": "O",
+    "Energy": "E",
+    "Fused Gate": "[]",
+}
 
 
 class H(Gate):
diff --git a/src/qibo/gates/special.py b/src/qibo/gates/special.py
index d152dd6bf..b8a0e9b2e 100644
--- a/src/qibo/gates/special.py
+++ b/src/qibo/gates/special.py
@@ -37,7 +37,7 @@ class FusedGate(SpecialGate):
 
     def __init__(self, *q):
         super().__init__()
-        self.name = "fused"
+        self.name = "Fused Gate"
         self.target_qubits = tuple(sorted(q))
         self.init_args = list(q)
         self.qubit_set = set(q)
@@ -75,9 +75,6 @@ class FusedGate(SpecialGate):
         else:
             self.gates.append(gate)
 
-    def __iter__(self):
-        return iter(self.gates)
-
     def _dagger(self):
         dagger = self.__class__(*self.init_args)
         for gate in self.gates[::-1]:
diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py
index 8d2c94ccf..461879153 100644
--- a/src/qibo/models/circuit.py
+++ b/src/qibo/models/circuit.py
@@ -1272,6 +1272,52 @@ class Circuit:
 
         return len(qubits), gate_list
 
+    def _update_draw_matrix(self, matrix, idx, gate, gate_symbol=None):
+        """Helper method for :meth:`qibo.models.circuit.Circuit.draw`."""
+        if gate_symbol is None:
+            if gate.name not in gates.DRAW_LABELS:  # pragma: no cover
+                raise_error(
+                    NotImplementedError,
+                    f"{gate.name} gate is not supported by `circuit.draw`",
+                )
+            gate_symbol = gates.DRAW_LABELS.get(gate.name)
+
+        if isinstance(gate, gates.CallbackGate):
+            targets = list(range(self.nqubits))
+        else:
+            targets = list(gate.target_qubits)
+        controls = list(gate.control_qubits)
+
+        # identify boundaries
+        qubits = targets + controls
+        qubits.sort()
+        min_qubits_id = qubits[0]
+        max_qubits_id = qubits[-1]
+
+        # identify column
+        col = idx[targets[0]] if not controls and len(targets) == 1 else max(idx)
+
+        # extend matrix
+        for iq in range(self.nqubits):
+            matrix[iq].extend((1 + col - len(matrix[iq])) * [""])
+
+        # fill
+        for iq in range(min_qubits_id, max_qubits_id + 1):
+            if iq in targets:
+                matrix[iq][col] = gate_symbol
+            elif iq in controls:
+                matrix[iq][col] = "o"
+            else:
+                matrix[iq][col] = "|"
+
+        # update indexes
+        if not controls and len(targets) == 1:
+            idx[targets[0]] += 1
+        else:
+            idx = [col + 1] * self.nqubits
+
+        return matrix, idx
+
     def draw(self, line_wrap=70, legend=False) -> str:
         """Draw text circuit using unicode symbols.
 
@@ -1284,97 +1330,21 @@ class Circuit:
         Return:
             String containing text circuit diagram.
         """
-        labels = {
-            "h": "H",
-            "x": "X",
-            "y": "Y",
-            "z": "Z",
-            "s": "S",
-            "sdg": "SDG",
-            "t": "T",
-            "tdg": "TDG",
-            "rx": "RX",
-            "ry": "RY",
-            "rz": "RZ",
-            "u1": "U1",
-            "u2": "U2",
-            "u3": "U3",
-            "cx": "X",
-            "swap": "x",
-            "iswap": "i",
-            "cz": "Z",
-            "crx": "RX",
-            "cry": "RY",
-            "crz": "RZ",
-            "cu1": "U1",
-            "cu3": "U3",
-            "ccx": "X",
-            "id": "I",
-            "measure": "M",
-            "fsim": "f",
-            "generalizedfsim": "gf",
-            "rxx": "RXX",
-            "ryy": "RYY",
-            "rzz": "RZZ",
-            "Unitary": "U",
-            "fswap": "fx",
-            "PauliNoiseChannel": "PN",
-            "KrausChannel": "K",
-            "UnitaryChannel": "U",
-            "ThermalRelaxationChannel": "TR",
-            "DepolarizingChannel": "D",
-            "ResetChannel": "R",
-            "PartialTrace": "PT",
-            "EntanglementEntropy": "EE",
-            "Norm": "N",
-            "Overlap": "O",
-            "Energy": "E",
-        }
-
         # build string representation of gates
         matrix = [[] for _ in range(self.nqubits)]
         idx = [0] * self.nqubits
 
         for gate in self.queue:
-            if gate.name not in labels:  # pragma: no cover
-                raise_error(
-                    NotImplementedError,
-                    f"{gate.name} gate is not supported by `circuit.draw`",
-                )
-            gate_name = labels.get(gate.name)
-            if isinstance(gate, gates.CallbackGate):
-                targets = list(range(self.nqubits))
-            else:
-                targets = list(gate.target_qubits)
-            controls = list(gate.control_qubits)
-
-            # identify boundaries
-            qubits = targets + controls
-            qubits.sort()
-            min_qubits_id = qubits[0]
-            max_qubits_id = qubits[-1]
-
-            # identify column
-            col = idx[targets[0]] if not controls and len(targets) == 1 else max(idx)
-
-            # extend matrix
-            for iq in range(self.nqubits):
-                matrix[iq].extend((1 + col - len(matrix[iq])) * [""])
-
-            # fill
-            for iq in range(min_qubits_id, max_qubits_id + 1):
-                if iq in targets:
-                    matrix[iq][col] = gate_name
-                elif iq in controls:
-                    matrix[iq][col] = "o"
-                else:
-                    matrix[iq][col] = "|"
-
-            # update indexes
-            if not controls and len(targets) == 1:
-                idx[targets[0]] += 1
+            if isinstance(gate, gates.FusedGate):
+                # start fused gate
+                matrix, idx = self._update_draw_matrix(matrix, idx, gate, "[")
+                # draw gates contained in the fused gate
+                for subgate in gate.gates:
+                    matrix, idx = self._update_draw_matrix(matrix, idx, subgate)
+                # end fused gate
+                matrix, idx = self._update_draw_matrix(matrix, idx, gate, "]")
             else:
-                idx = [col + 1] * self.nqubits
+                matrix, idx = self._update_draw_matrix(matrix, idx, gate)
 
         # Include measurement gates
         if self.measurement_gate:
@@ -1407,11 +1377,11 @@ class Circuit:
             names = [
                 i.name
                 for i in self.queue
-                if isinstance(i, gates.CallbackGate) or "Channel" in i.name
+                if isinstance(i, gates.SpecialGate) or "Channel" in i.name
             ]
             names = list(dict.fromkeys(names))
             table = tabulate(
-                [[i, labels[i]] for i in names],
+                [[i, gates.DRAW_LABELS[i]] for i in names],
                 headers=["Gate", "Symbol"],
                 tablefmt="orgtbl",
             )


Test patch:
diff --git a/src/qibo/tests/test_models_circuit_fuse.py b/src/qibo/tests/test_models_circuit_fuse.py
index ab674f154..a3b031d3f 100644
--- a/src/qibo/tests/test_models_circuit_fuse.py
+++ b/src/qibo/tests/test_models_circuit_fuse.py
@@ -186,3 +186,42 @@ def test_set_parameters_fusion(backend):
     c.set_parameters(4 * [0.4321])
     fused_c.set_parameters(4 * [0.4321])
     backend.assert_circuitclose(fused_c, c)
+
+
+def test_add_fused_gate(backend):
+    """Check adding fused gate to a circuit."""
+    c = Circuit(2)
+    c.add(gates.H(0))
+    c.add(gates.H(1))
+    c.add(gates.CNOT(0, 1))
+    fused_c = c.fuse()
+    fgate = fused_c.queue[0]
+    assert isinstance(fgate, gates.FusedGate)
+
+    new_c = Circuit(2)
+    new_c.add(fgate)
+    assert c.depth == 2
+    assert c.ngates == 3
+    assert new_c.depth == 1
+    assert new_c.ngates == 1
+    backend.assert_circuitclose(fused_c, c)
+    backend.assert_circuitclose(new_c, c)
+
+
+def test_fused_gate_draw():
+    ref = (
+        "q0: ─[─H─U1───]─U1─U1─U1─────────────────────────────────────x───\n"
+        "q1: ─[───o──H─]─|──|──|──[─U1───]─U1─U1──────────────────────|─x─\n"
+        "q2: ────────────o──|──|──[─o──H─]─|──|──[─U1───]─U1──────────|─|─\n"
+        "q3: ───────────────o──|───────────o──|──[─o──H─]─|──[─U1───]─|─x─\n"
+        "q4: ──────────────────o──────────────o───────────o──[─o──H─]─x───"
+    )
+    circuit = Circuit(5)
+    for i1 in range(5):
+        circuit.add(gates.H(i1))
+        for i2 in range(i1 + 1, 5):
+            circuit.add(gates.CU1(i2, i1, theta=0))
+    circuit.add(gates.SWAP(0, 4))
+    circuit.add(gates.SWAP(1, 3))
+    circuit = circuit.fuse()
+    assert circuit.draw() == ref