Repository: Qiskit/qiskit

Issue:
add visualization of open controlled gates [text drawer]
In PR #3739, open controlled gates were implemented. With that PR a new keyword argument was added to specify the control state,

`XGate().control(3, ctrl_state=6)`

This creates a 4 qubit gate where the first three qubits are controls and the fourth qubit is the target where the x-gate may be applied. A `ctrl_state` may alternatively be expressed as an integer string,

`XGate().control(3, ctrl_state='110')`

where it is more apparent which qubits are open and closed. In this case when q[0] is open and q[1] and q[2] are closed the x-gate will be applied to q[4].

<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected behavior?





Discussion:
Is this the expected result?
```
circuit.append(XGate().control(3, ctrl_state='110'), [qr[0], qr[3], qr[2], qr[1]])
```
```
q_0: |0>──■──
        ┌─┴─┐
q_1: |0>┤ X ├
        └─┬─┘
q_2: |0>──o──
          │
q_3: |0>──■──
```
q_0 should be open and q_2 and q_3 closed.
Gotcha
```
q_0: |0>──o──
        ┌─┴─┐
q_1: |0>┤ X ├
        └─┬─┘
q_2: |0>──■──
          │
q_3: |0>──■──
```
yeah left-most bit corresponds to lowest line in the drawing.

Resolving patch:
diff --git a/qiskit/visualization/text.py b/qiskit/visualization/text.py
--- a/qiskit/visualization/text.py
+++ b/qiskit/visualization/text.py
@@ -259,10 +259,10 @@ def __init__(self, label, input_length, order, wire_label=''):
 class BoxOnQuWireMid(BoxOnWireMid, BoxOnQuWire):
     """ Draws the middle part of a box that affects more than one quantum wire"""
 
-    def __init__(self, label, input_length, order, wire_label='', control_label=False):
+    def __init__(self, label, input_length, order, wire_label='', control_label=None):
         super().__init__(label, input_length, order, wire_label=wire_label)
         if control_label:
-            self.mid_format = "■{} %s ├".format(self.wire_label)
+            self.mid_format = "{}{} %s ├".format(control_label, self.wire_label)
         else:
             self.mid_format = "┤{} %s ├".format(self.wire_label)
 
@@ -401,8 +401,25 @@ def __init__(self, top_connect="", bot_connect="", conditional=False):
         self.mid_bck = '─'
 
 
+class OpenBullet(DirectOnQuWire):
+    """Draws an open bullet (usually with a connector). E.g. the top part of a CX gate.
+
+    ::
+
+        top:
+        mid: ─o─  ───o───
+        bot:  │      │
+    """
+
+    def __init__(self, top_connect="", bot_connect="", conditional=False):
+        super().__init__('o')
+        self.top_connect = top_connect
+        self.bot_connect = '│' if conditional else bot_connect
+        self.mid_bck = '─'
+
+
 class EmptyWire(DrawElement):
-    """ This element is just the wire, with no instructions nor operations."""
+    """This element is just the wire, with no instructions nor operations."""
 
     def __init__(self, wire):
         super().__init__(wire)
@@ -756,7 +773,7 @@ def merge_lines(top, bot, icod="top"):
                 ret += "╬"
             elif topc in "║╥" and botc in "─":
                 ret += "╫"
-            elif topc in '╫╬' and botc in " ":
+            elif topc in '║╫╬' and botc in " ":
                 ret += "║"
             elif topc == '└' and botc == "┌":
                 ret += "├"
@@ -795,13 +812,15 @@ def controlled_wires(instruction, layer):
 
         Returns:
             Tuple(list, list, list):
-              - controlled arguments on top of the "instruction box"
-              - controlled arguments on bottom of the "instruction box"
-              - controlled arguments in the "instruction box"
+              - tuple: controlled arguments on top of the "instruction box", and its status
+              - tuple: controlled arguments on bottom of the "instruction box", and its status
+              - tuple: controlled arguments in the "instruction box", and its status
               - the rest of the arguments
         """
-        ctrl_qubits = instruction.qargs[:instruction.op.num_ctrl_qubits]
-        args_qubits = instruction.qargs[instruction.op.num_ctrl_qubits:]
+        num_ctrl_qubits = instruction.op.num_ctrl_qubits
+        ctrl_qubits = instruction.qargs[:num_ctrl_qubits]
+        args_qubits = instruction.qargs[num_ctrl_qubits:]
+        ctrl_state = "{0:b}".format(instruction.op.ctrl_state).rjust(num_ctrl_qubits, '0')[::-1]
 
         in_box = list()
         top_box = list()
@@ -809,10 +828,10 @@ def controlled_wires(instruction, layer):
 
         qubit_index = sorted([i for i, x in enumerate(layer.qregs) if x in args_qubits])
 
-        for ctrl_qubit in ctrl_qubits:
-            if min(qubit_index) > layer.qregs.index(ctrl_qubit):
+        for ctrl_qubit in zip(ctrl_qubits, ctrl_state):
+            if min(qubit_index) > layer.qregs.index(ctrl_qubit[0]):
                 top_box.append(ctrl_qubit)
-            elif max(qubit_index) < layer.qregs.index(ctrl_qubit):
+            elif max(qubit_index) < layer.qregs.index(ctrl_qubit[0]):
                 bot_box.append(ctrl_qubit)
             else:
                 in_box.append(ctrl_qubit)
@@ -886,16 +905,11 @@ def add_connected_gate(instruction, gates, layer, current_cons):
             gates = [Bullet(conditional=conditional), BoxOnQuWire('Y')]
             add_connected_gate(instruction, gates, layer, current_cons)
 
-        elif instruction.name == 'cz':
-            # cz
+        elif instruction.name == 'cz' and instruction.op.ctrl_state == 1:
+            # cz TODO: only supports one closed controlled for now
             gates = [Bullet(conditional=conditional), Bullet(conditional=conditional)]
             add_connected_gate(instruction, gates, layer, current_cons)
 
-        elif instruction.name == 'ch':
-            # ch
-            gates = [Bullet(conditional=conditional), BoxOnQuWire('H')]
-            add_connected_gate(instruction, gates, layer, current_cons)
-
         elif instruction.name == 'cu1':
             # cu1
             connection_label = TextDrawing.params_for_label(instruction)[0]
@@ -933,8 +947,11 @@ def add_connected_gate(instruction, gates, layer, current_cons):
 
             params_array = TextDrawing.controlled_wires(instruction, layer)
             controlled_top, controlled_bot, controlled_edge, rest = params_array
-            for _ in controlled_top + controlled_bot + controlled_edge:
-                gates.append(Bullet(conditional=conditional))
+            for i in controlled_top + controlled_bot + controlled_edge:
+                if i[1] == '1':
+                    gates.append(Bullet(conditional=conditional))
+                else:
+                    gates.append(OpenBullet(conditional=conditional))
             if len(rest) > 1:
                 top_connect = '┴' if controlled_top else None
                 bot_connect = '┬' if controlled_bot else None
@@ -1093,11 +1110,12 @@ def _set_multibox(self, label, qubits=None, clbits=None, top_connect=None,
         else:
             raise VisualizationError("_set_multibox error!.")
 
+        control_index = {}
         if controlled_edge:
-            control_index = [i for i, x in enumerate(self.qregs) if x in controlled_edge]
-        else:
-            control_index = []
-
+            for index, qubit in enumerate(self.qregs):
+                for qubit_in_edge, value in controlled_edge:
+                    if qubit == qubit_in_edge:
+                        control_index[index] = '■' if value == '1' else 'o'
         if len(bit_index) == 1:
             set_bit(bits[0], OnWire(label, top_connect=top_connect))
         else:
@@ -1110,8 +1128,10 @@ def _set_multibox(self, label, qubits=None, clbits=None, top_connect=None,
                 else:
                     named_bit = (self.qregs + self.cregs)[bit_i]
                     wire_label = ' ' * len(qargs[0])
+
+                control_label = control_index.get(bit_i)
                 set_bit(named_bit, OnWireMid(label, box_height, order, wire_label=wire_label,
-                                             control_label=bit_i in control_index))
+                                             control_label=control_label))
             set_bit(bits.pop(0), OnWireBot(label, box_height, bot_connect=bot_connect,
                                            wire_label=qargs.pop(0), conditional=conditional))
         return bit_index


Test patch:
diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py
--- a/test/python/visualization/test_circuit_text_drawer.py
+++ b/test/python/visualization/test_circuit_text_drawer.py
@@ -29,7 +29,7 @@
 from qiskit.transpiler import Layout
 from qiskit.visualization import text as elements
 from qiskit.visualization.circuit_visualization import _text_circuit_drawer
-from qiskit.extensions import HGate, U2Gate, XGate, UnitaryGate, CZGate
+from qiskit.extensions import HGate, U2Gate, XGate, UnitaryGate, CZGate, ZGate
 
 
 class TestTextDrawerElement(QiskitTestCase):
@@ -1083,6 +1083,36 @@ def test_text_conditional_1(self):
         self.assertEqual(str(_text_circuit_drawer(circuit,
                                                   vertical_compression='medium')), expected)
 
+    def test_text_measure_with_spaces(self):
+        """Measure wire might have extra spaces
+        Found while reproducing
+        https://quantumcomputing.stackexchange.com/q/10194/1859"""
+        qasm_string = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[2];
+            creg c[3];
+            measure q[0] -> c[1];
+            if(c==1) x q[1];
+        """
+        expected = '\n'.join(["        ┌─┐       ",
+                              "q_0: |0>┤M├───────",
+                              "        └╥┘       ",
+                              "         ║  ┌───┐ ",
+                              "q_1: |0>─╫──┤ X ├─",
+                              "         ║  └─┬─┘ ",
+                              "         ║ ┌──┴──┐",
+                              " c_0: 0 ═╬═╡     ╞",
+                              "         ║ │     │",
+                              "         ║ │     │",
+                              " c_1: 0 ═╩═╡ = 1 ╞",
+                              "           │     │",
+                              "           │     │",
+                              " c_2: 0 ═══╡     ╞",
+                              "           └─────┘"])
+        circuit = QuantumCircuit.from_qasm_str(qasm_string)
+        self.assertEqual(str(_text_circuit_drawer(circuit, vertical_compression='low')), expected)
+
 
 class TestTextConditional(QiskitTestCase):
     """Gates with conditionals"""
@@ -1868,6 +1898,243 @@ def test_controlled_composite_gate_all(self):
         self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
 
 
+class TestTextOpenControlledGate(QiskitTestCase):
+    """Test open controlled gates"""
+
+    def test_ch_bot(self):
+        """Open controlled H (bottom)"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "        ┌─┴─┐",
+                              "q_1: |0>┤ H ├",
+                              "        └───┘"])
+        qr = QuantumRegister(2, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(1, ctrl_state=0), [qr[0], qr[1]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_cz_bot(self):
+        """Open controlled Z (bottom)"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "        ┌─┴─┐",
+                              "q_1: |0>┤ Z ├",
+                              "        └───┘"])
+        qr = QuantumRegister(2, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(ZGate().control(1, ctrl_state=0), [qr[0], qr[1]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_cch_bot(self):
+        """Controlled CH (bottom)"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "          │  ",
+                              "q_1: |0>──■──",
+                              "        ┌─┴─┐",
+                              "q_2: |0>┤ H ├",
+                              "        └───┘"])
+        qr = QuantumRegister(3, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(2, ctrl_state='10'), [qr[0], qr[1], qr[2]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_cch_mid(self):
+        """Controlled CH (middle)"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "        ┌─┴─┐",
+                              "q_1: |0>┤ H ├",
+                              "        └─┬─┘",
+                              "q_2: |0>──■──",
+                              "             "])
+        qr = QuantumRegister(3, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(2, ctrl_state='10'), [qr[0], qr[2], qr[1]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_cch_top(self):
+        """Controlled CH"""
+        expected = '\n'.join(["        ┌───┐",
+                              "q_0: |0>┤ H ├",
+                              "        └─┬─┘",
+                              "q_1: |0>──o──",
+                              "          │  ",
+                              "q_2: |0>──■──",
+                              "             "])
+        qr = QuantumRegister(3, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(2, ctrl_state='10'), [qr[1], qr[2], qr[0]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_c3h(self):
+        """Controlled Controlled CH"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "          │  ",
+                              "q_1: |0>──o──",
+                              "          │  ",
+                              "q_2: |0>──■──",
+                              "        ┌─┴─┐",
+                              "q_3: |0>┤ H ├",
+                              "        └───┘"])
+        qr = QuantumRegister(4, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(3, ctrl_state='100'), [qr[0], qr[1], qr[2], qr[3]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_c3h_middle(self):
+        """Controlled Controlled CH (middle)"""
+        expected = '\n'.join(["             ",
+                              "q_0: |0>──o──",
+                              "        ┌─┴─┐",
+                              "q_1: |0>┤ H ├",
+                              "        └─┬─┘",
+                              "q_2: |0>──o──",
+                              "          │  ",
+                              "q_3: |0>──■──",
+                              "             "])
+        qr = QuantumRegister(4, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(HGate().control(3, ctrl_state='010'), [qr[0], qr[3], qr[2], qr[1]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_c3u2(self):
+        """Controlled Controlled U2"""
+        expected = '\n'.join([
+            "                         ",
+            "q_0: |0>────────o────────",
+            "        ┌───────┴───────┐",
+            "q_1: |0>┤ U2(pi,-5pi/8) ├",
+            "        └───────┬───────┘",
+            "q_2: |0>────────■────────",
+            "                │        ",
+            "q_3: |0>────────o────────",
+            "                         "])
+        qr = QuantumRegister(4, 'q')
+        circuit = QuantumCircuit(qr)
+        circuit.append(U2Gate(pi, -5 * pi / 8).control(3, ctrl_state='100'),
+                       [qr[0], qr[3], qr[2], qr[1]])
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_controlled_composite_gate_edge(self):
+        """Controlled composite gates (edge)
+        See: https://github.com/Qiskit/qiskit-terra/issues/3546 """
+        expected = '\n'.join(["        ┌──────┐",
+                              "q_0: |0>┤0     ├",
+                              "        │      │",
+                              "q_1: |0>o      ├",
+                              "        │  Ghz │",
+                              "q_2: |0>┤1     ├",
+                              "        │      │",
+                              "q_3: |0>┤2     ├",
+                              "        └──────┘"])
+        ghz_circuit = QuantumCircuit(3, name='ghz')
+        ghz_circuit.h(0)
+        ghz_circuit.cx(0, 1)
+        ghz_circuit.cx(1, 2)
+        ghz = ghz_circuit.to_gate()
+        cghz = ghz.control(1, ctrl_state='0')
+        circuit = QuantumCircuit(4)
+        circuit.append(cghz, [1, 0, 2, 3])
+
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_controlled_composite_gate_top(self):
+        """Controlled composite gates (top) """
+        expected = '\n'.join(["                ",
+                              "q_0: |0>───o────",
+                              "        ┌──┴───┐",
+                              "q_1: |0>┤0     ├",
+                              "        │      │",
+                              "q_2: |0>┤2 Ghz ├",
+                              "        │      │",
+                              "q_3: |0>┤1     ├",
+                              "        └──────┘"])
+        ghz_circuit = QuantumCircuit(3, name='ghz')
+        ghz_circuit.h(0)
+        ghz_circuit.cx(0, 1)
+        ghz_circuit.cx(1, 2)
+        ghz = ghz_circuit.to_gate()
+        cghz = ghz.control(1, ctrl_state='0')
+        circuit = QuantumCircuit(4)
+        circuit.append(cghz, [0, 1, 3, 2])
+
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_controlled_composite_gate_bot(self):
+        """Controlled composite gates (bottom) """
+        expected = '\n'.join(["        ┌──────┐",
+                              "q_0: |0>┤1     ├",
+                              "        │      │",
+                              "q_1: |0>┤0 Ghz ├",
+                              "        │      │",
+                              "q_2: |0>┤2     ├",
+                              "        └──┬───┘",
+                              "q_3: |0>───o────",
+                              "                "])
+        ghz_circuit = QuantumCircuit(3, name='ghz')
+        ghz_circuit.h(0)
+        ghz_circuit.cx(0, 1)
+        ghz_circuit.cx(1, 2)
+        ghz = ghz_circuit.to_gate()
+        cghz = ghz.control(1, ctrl_state='0')
+        circuit = QuantumCircuit(4)
+        circuit.append(cghz, [3, 1, 0, 2])
+
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_controlled_composite_gate_top_bot(self):
+        """Controlled composite gates (top and bottom) """
+        expected = '\n'.join(["                ",
+                              "q_0: |0>───■────",
+                              "        ┌──┴───┐",
+                              "q_1: |0>┤0     ├",
+                              "        │      │",
+                              "q_2: |0>┤1 Ghz ├",
+                              "        │      │",
+                              "q_3: |0>┤2     ├",
+                              "        └──┬───┘",
+                              "q_4: |0>───o────",
+                              "                "])
+        ghz_circuit = QuantumCircuit(3, name='ghz')
+        ghz_circuit.h(0)
+        ghz_circuit.cx(0, 1)
+        ghz_circuit.cx(1, 2)
+        ghz = ghz_circuit.to_gate()
+        ccghz = ghz.control(2, ctrl_state='01')
+        circuit = QuantumCircuit(5)
+        circuit.append(ccghz, [4, 0, 1, 2, 3])
+
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+    def test_controlled_composite_gate_all(self):
+        """Controlled composite gates (top, bot, and edge) """
+        expected = '\n'.join(["                ",
+                              "q_0: |0>───o────",
+                              "        ┌──┴───┐",
+                              "q_1: |0>┤0     ├",
+                              "        │      │",
+                              "q_2: |0>o      ├",
+                              "        │  Ghz │",
+                              "q_3: |0>┤1     ├",
+                              "        │      │",
+                              "q_4: |0>┤2     ├",
+                              "        └──┬───┘",
+                              "q_5: |0>───o────",
+                              "                "])
+        ghz_circuit = QuantumCircuit(3, name='ghz')
+        ghz_circuit.h(0)
+        ghz_circuit.cx(0, 1)
+        ghz_circuit.cx(1, 2)
+        ghz = ghz_circuit.to_gate()
+        ccghz = ghz.control(3, ctrl_state='000')
+        circuit = QuantumCircuit(6)
+        circuit.append(ccghz, [0, 2, 5, 1, 3, 4])
+
+        self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
+
+
 class TestTextWithLayout(QiskitTestCase):
     """The with_layout option"""