diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index 0cb97755efac..883e9cb6527c 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -26,7 +26,7 @@ Measure, ) from qiskit.circuit.library import PauliEvolutionGate -from qiskit.circuit import ClassicalRegister, QuantumCircuit +from qiskit.circuit import ClassicalRegister, QuantumCircuit, Qubit, ControlFlowOp from qiskit.circuit.tools import pi_check from qiskit.converters import circuit_to_dag from qiskit.utils import optionals as _optionals @@ -361,7 +361,7 @@ def generate_latex_label(label): def _get_layered_instructions( - circuit, reverse_bits=False, justify=None, idle_wires=True, wire_order=None + circuit, reverse_bits=False, justify=None, idle_wires=True, wire_order=None, wire_map=None ): """ Given a circuit, return a tuple (qubits, clbits, nodes) where @@ -390,7 +390,10 @@ def _get_layered_instructions( # default to left justify = justify if justify in ("right", "none") else "left" - qubits = circuit.qubits.copy() + if wire_map is not None: + qubits = [bit for bit in wire_map if isinstance(bit, Qubit)] + else: + qubits = circuit.qubits.copy() clbits = circuit.clbits.copy() nodes = [] @@ -424,7 +427,7 @@ def _get_layered_instructions( for node in dag.topological_op_nodes(): nodes.append([node]) else: - nodes = _LayerSpooler(dag, justify, measure_map) + nodes = _LayerSpooler(dag, justify, measure_map, wire_map) # Optionally remove all idle wires and instructions that are on them and # on them only. @@ -450,7 +453,7 @@ def _sorted_nodes(dag_layer): return nodes -def _get_gate_span(qubits, node): +def _get_gate_span(qubits, node, wire_map): """Get the list of qubits drawing this gate would cover qiskit-terra #2802 """ @@ -464,26 +467,32 @@ def _get_gate_span(qubits, node): if index > max_index: max_index = index - if node.cargs or getattr(node.op, "condition", None): - return qubits[min_index : len(qubits)] + # Because of wrapping boxes for mpl control flow ops, this + # type of op must be the only op in the layer + if wire_map is not None and isinstance(node.op, ControlFlowOp): + span = qubits + elif node.cargs or getattr(node.op, "condition", None): + span = qubits[min_index : len(qubits)] + else: + span = qubits[min_index : max_index + 1] - return qubits[min_index : max_index + 1] + return span -def _any_crossover(qubits, node, nodes): +def _any_crossover(qubits, node, nodes, wire_map): """Return True .IFF. 'node' crosses over any 'nodes'.""" - gate_span = _get_gate_span(qubits, node) + gate_span = _get_gate_span(qubits, node, wire_map) all_indices = [] for check_node in nodes: if check_node != node: - all_indices += _get_gate_span(qubits, check_node) + all_indices += _get_gate_span(qubits, check_node, wire_map) return any(i in gate_span for i in all_indices) class _LayerSpooler(list): """Manipulate list of layer dicts for _get_layered_instructions.""" - def __init__(self, dag, justification, measure_map): + def __init__(self, dag, justification, measure_map, wire_map): """Create spool""" super().__init__() self.dag = dag @@ -491,6 +500,7 @@ def __init__(self, dag, justification, measure_map): self.clbits = dag.clbits self.justification = justification self.measure_map = measure_map + self.wire_map = wire_map self.cregs = [self.dag.cregs[reg] for reg in self.dag.cregs] if self.justification == "left": @@ -523,7 +533,7 @@ def is_found_in(self, node, nodes): def insertable(self, node, nodes): """True .IFF. we can add 'node' to layer 'nodes'""" - return not _any_crossover(self.qubits, node, nodes) + return not _any_crossover(self.qubits, node, nodes, self.wire_map) def slide_from_left(self, node, index): """Insert node into first layer where there is no conflict going l > r""" diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 965dec412337..983311f067c8 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -20,7 +20,19 @@ import numpy as np -from qiskit.circuit import ControlledGate, Qubit, Clbit, ClassicalRegister, Measure +from qiskit.circuit import ( + QuantumCircuit, + Qubit, + Clbit, + ClassicalRegister, + ControlledGate, + Measure, + ControlFlowOp, + WhileLoopOp, + IfElseOp, + ForLoopOp, + SwitchCaseOp, +) from qiskit.circuit.library.standard_gates import ( SwapGate, RZZGate, @@ -42,6 +54,7 @@ get_bit_reg_index, get_wire_label, get_condition_label_val, + _get_layered_instructions, ) from ..utils import matplotlib_close_if_inline @@ -49,11 +62,16 @@ WID = 0.65 HIG = 0.65 -PORDER_GATE = 5 -PORDER_LINE = 3 -PORDER_REGLINE = 2 -PORDER_GRAY = 3 -PORDER_TEXT = 6 +# Z dimension order for different drawing types +PORDER_REGLINE = 1 +PORDER_FLOW = 3 +PORDER_MASK = 4 +PORDER_LINE = 6 +PORDER_LINE_PLUS = 7 +PORDER_BARRIER = 8 +PORDER_GATE = 10 +PORDER_GATE_PLUS = 11 +PORDER_TEXT = 13 INFINITE_FOLD = 10000000 @@ -126,6 +144,15 @@ def __init__( self._lwidth1 = 1.0 self._lwidth15 = 1.5 self._lwidth2 = 2.0 + self._lwidth3 = 3.0 + self._lwidth4 = 4.0 + + # Class instances of MatplotlibDrawer for each flow gate - If/Else, For, While, Switch + self._flow_drawers = {} + + # Set if gate is inside a flow gate + self._flow_parent = None + self._flow_wire_map = {} # _char_list for finding text_width of names, labels, and params self._char_list = { @@ -235,7 +262,7 @@ def draw(self, filename=None, verbose=False): from matplotlib import pyplot as plt # glob_data contains global values used throughout, "n_lines", "x_offset", "next_x_index", - # "patches_mod", subfont_factor" + # "patches_mod", "subfont_factor" glob_data = {} glob_data["patches_mod"] = patches @@ -267,9 +294,9 @@ def draw(self, filename=None, verbose=False): # load the wire map wire_map = get_wire_map(self._circuit, self._qubits + self._clbits, self._cregbundle) - # node_data per node with 'width', 'gate_text', 'raw_gate_text', - # 'ctrl_text', 'param_text', q_xy', and 'c_xy', - # and colors 'fc', 'ec', 'lc', 'sc', 'gt', and 'tc' + # node_data per node with "width", "gate_text", "raw_gate_text", "ctrl_text", + # "param_text", "nest_depth", "inside_flow", "x_index", "indexset", "jump_values", + # "case_num", "q_xy", "c_xy", and colors "fc", "ec", "lc", "sc", "gt", and "tc" node_data = {} # dicts for the names and locations of register/bit labels @@ -279,10 +306,11 @@ def draw(self, filename=None, verbose=False): # load the _qubit_dict and _clbit_dict with register info self._set_bit_reg_info(wire_map, qubits_dict, clbits_dict, glob_data) - # get layer widths - layer_widths = self._get_layer_widths(node_data, glob_data) + # get layer widths - flow gates are initialized here + layer_widths = self._get_layer_widths(node_data, wire_map, glob_data) - # load the coordinates for each gate and compute number of folds + # load the coordinates for each top level gate and compute number of folds. + # coordinates for flow gates are loaded before draw_ops max_x_index = self._get_coords( node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data ) @@ -341,6 +369,8 @@ def draw(self, filename=None, verbose=False): self._lwidth1 = 1.0 * scale self._lwidth15 = 1.5 * scale self._lwidth2 = 2.0 * scale + self._lwidth3 = 3.0 * scale + self._lwidth4 = 4.0 * scale # Once the scaling factor has been determined, the global phase, register names # and numbers, wires, and gates are drawn @@ -348,9 +378,15 @@ def draw(self, filename=None, verbose=False): plt_mod.text(xl, yt, "Global Phase: %s" % pi_check(self._global_phase, output="mpl")) self._draw_regs_wires(num_folds, xmax, max_x_index, qubits_dict, clbits_dict, glob_data) self._draw_ops( - self._nodes, node_data, wire_map, layer_widths, clbits_dict, glob_data, verbose + self._nodes, + node_data, + wire_map, + layer_widths, + qubits_dict, + clbits_dict, + glob_data, + verbose, ) - if filename: mpl_figure.savefig( filename, @@ -362,7 +398,7 @@ def draw(self, filename=None, verbose=False): matplotlib_close_if_inline(mpl_figure) return mpl_figure - def _get_layer_widths(self, node_data, glob_data): + def _get_layer_widths(self, node_data, wire_map, glob_data): """Compute the layer_widths for the layers""" layer_widths = {} @@ -373,7 +409,7 @@ def _get_layer_widths(self, node_data, glob_data): # so that layer widths are not counted more than once if i != 0: layer_num = -1 - layer_widths[node] = [1, layer_num] + layer_widths[node] = [1, layer_num, self._flow_parent] op = node.op node_data[node] = {} @@ -411,12 +447,12 @@ def _get_layer_widths(self, node_data, glob_data): ctrl_width = ( self._get_text_width(ctrl_text, glob_data, fontsize=self._style["sfs"]) - 0.05 ) - - # get param_width, but 0 for gates with array params + # get param_width, but 0 for gates with array params or circuits in params if ( hasattr(op, "params") and len(op.params) > 0 and not any(isinstance(param, np.ndarray) for param in op.params) + and not any(isinstance(param, QuantumCircuit) for param in op.params) ): param_text = get_param_str(op, "mpl", ndigits=3) if isinstance(op, Initialize): @@ -441,6 +477,92 @@ def _get_layer_widths(self, node_data, glob_data): ) gate_width = (raw_gate_width + 0.08) * 1.58 + # Check if a ControlFlowOp - node_data load for these gates is done here + elif isinstance(node.op, ControlFlowOp): + self._flow_drawers[node] = [] + node_data[node]["width"] = [] + node_data[node]["nest_depth"] = 0 + gate_width = 0.0 + + # Get the list of circuits to iterate over from the blocks + circuit_list = list(node.op.blocks) + + # params is [indexset, loop_param, circuit] for for_loop, + # op.cases_specifier() returns jump tuple and circuit for switch/case + if isinstance(op, ForLoopOp): + node_data[node]["indexset"] = op.params[0] + elif isinstance(op, SwitchCaseOp): + node_data[node]["jump_values"] = [] + cases = list(op.cases_specifier()) + + # Create an empty circuit at the head of the circuit_list if a Switch box + circuit_list.insert(0, cases[0][1].copy_empty_like()) + for jump_values, _ in cases: + node_data[node]["jump_values"].append(jump_values) + + # Now process the circuits inside the ControlFlowOps + for circ_num, circuit in enumerate(circuit_list): + raw_gate_width = 0.0 + + # Depth of nested ControlFlowOp used for color of box + if self._flow_parent is not None: + node_data[node]["nest_depth"] = ( + node_data[self._flow_parent]["nest_depth"] + 1 + ) + # Update the wire_map with the qubits from the inner circuit + flow_wire_map = { + inner: wire_map[outer] + for outer, inner in zip(self._qubits, circuit.qubits) + if inner not in wire_map + } + if not flow_wire_map: + flow_wire_map = wire_map + + # Get the layered node lists and instantiate a new drawer class for + # the circuit inside the ControlFlowOp. + qubits, clbits, nodes = _get_layered_instructions( + circuit, wire_map=flow_wire_map + ) + flow_drawer = MatplotlibDrawer( + qubits, + clbits, + nodes, + circuit, + style=self._style, + plot_barriers=self._plot_barriers, + fold=self._fold, + cregbundle=self._cregbundle, + ) + + # flow_parent is the parent of the new class instance + flow_drawer._flow_parent = node + flow_drawer._flow_wire_map = flow_wire_map + self._flow_drawers[node].append(flow_drawer) + + # Recursively call _get_layer_widths for the circuit inside the ControlFlowOp + flow_widths = flow_drawer._get_layer_widths(node_data, wire_map, glob_data) + layer_widths.update(flow_widths) + + # Gates within a SwitchCaseOp need to know which case they are in + for flow_layer in nodes: + for flow_node in flow_layer: + if isinstance(node.op, SwitchCaseOp): + node_data[flow_node]["case_num"] = circ_num + + # Add up the width values of the same flow_parent that are not -1 + # to get the raw_gate_width + for width, layer_num, flow_parent in flow_widths.values(): + if layer_num != -1 and flow_parent == flow_drawer._flow_parent: + raw_gate_width += width + + # Need extra incr of 1.0 for else and case boxes + gate_width += raw_gate_width + (1.0 if circ_num > 0 else 0.0) + + # Minor adjustment so else and case section gates align with indexes + if circ_num > 0: + raw_gate_width += 0.045 + node_data[node]["width"].append(raw_gate_width) + # Otherwise, standard gate or multiqubit gate else: raw_gate_width = self._get_text_width( @@ -454,7 +576,8 @@ def _get_layer_widths(self, node_data, glob_data): box_width = max(gate_width, ctrl_width, param_width, WID) if box_width > widest_box: widest_box = box_width - node_data[node]["width"] = max(raw_gate_width, raw_param_width) + if not isinstance(node.op, ControlFlowOp): + node_data[node]["width"] = max(raw_gate_width, raw_param_width) for node in layer: layer_widths[node][0] = int(widest_box) + 1 @@ -474,12 +597,18 @@ def _set_bit_reg_info(self, wire_map, qubits_dict, clbits_dict, glob_data): # if it's a creg, register is the key and just load the index if isinstance(wire, ClassicalRegister): + # If wire came from ControlFlowOp and not in clbits, don't draw it + if wire[0] not in self._clbits: + continue register = wire index = wire_map[wire] # otherwise, get the register from find_bit and use bit_index if # it's a bit, or the index of the bit in the register if it's a reg else: + # If wire came from ControlFlowOp and not in qubits or clbits, don't draw it + if wire not in self._qubits + self._clbits: + continue register, bit_index, reg_index = get_bit_reg_index(self._circuit, wire) index = bit_index if register is None else reg_index @@ -531,7 +660,17 @@ def _set_bit_reg_info(self, wire_map, qubits_dict, clbits_dict, glob_data): } glob_data["x_offset"] = -1.2 + longest_wire_label_width - def _get_coords(self, node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data): + def _get_coords( + self, + node_data, + wire_map, + layer_widths, + qubits_dict, + clbits_dict, + glob_data, + flow_parent=None, + is_not_first_block=None, + ): """Load all the coordinate info needed to place the gates on the drawing.""" prev_x_index = -1 @@ -539,6 +678,23 @@ def _get_coords(self, node_data, wire_map, layer_widths, qubits_dict, clbits_dic curr_x_index = prev_x_index + 1 l_width = [] for node in layer: + # For gates inside if, else, while, or case set the x_index and if it's an + # else or case increment by if width. For additional cases increment by + # width of previous cases. + if flow_parent is not None: + node_data[node]["x_index"] = ( + node_data[flow_parent]["x_index"] + curr_x_index + 1 + ) + if is_not_first_block: + # Add index space for else or first case if switch/case + node_data[node]["x_index"] += int(node_data[flow_parent]["width"][0]) + 1 + + # Add index space for remaining cases for switch/case + if "case_num" in node_data[node] and node_data[node]["case_num"] > 1: + for width in node_data[flow_parent]["width"][ + 1 : node_data[node]["case_num"] + ]: + node_data[node]["x_index"] += int(width) + 1 # get qubit indexes q_indxs = [] @@ -556,30 +712,42 @@ def _get_coords(self, node_data, wire_map, layer_widths, qubits_dict, clbits_dic else: c_indxs.append(wire_map[carg]) + flow_op = isinstance(node.op, ControlFlowOp) + if flow_parent is not None: + node_data[node]["inside_flow"] = True + x_index = node_data[node]["x_index"] + else: + node_data[node]["inside_flow"] = False + x_index = curr_x_index + # qubit coordinates node_data[node]["q_xy"] = [ self._plot_coord( - curr_x_index, + x_index, qubits_dict[ii]["y"], layer_widths[node][0], glob_data, + flow_op, ) for ii in q_indxs ] # clbit coordinates node_data[node]["c_xy"] = [ self._plot_coord( - curr_x_index, + x_index, clbits_dict[ii]["y"], layer_widths[node][0], glob_data, + flow_op, ) for ii in c_indxs ] # update index based on the value from plotting - curr_x_index = glob_data["next_x_index"] + if flow_parent is None: + curr_x_index = glob_data["next_x_index"] l_width.append(layer_widths[node][0]) + node_data[node]["x_index"] = x_index # adjust the column if there have been barriers encountered, but not plotted barrier_offset = 0 @@ -680,7 +848,7 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic [glob_data["x_offset"] + 0.2, glob_data["x_offset"] + 0.3], [y - 0.1, y + 0.1], color=self._style["cc"], - zorder=PORDER_LINE, + zorder=PORDER_REGLINE, ) self._ax.text( glob_data["x_offset"] + 0.1, @@ -727,7 +895,7 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic [ypos1, ypos2], color=self._style["lc"], linewidth=self._lwidth15, - zorder=PORDER_LINE, + zorder=PORDER_REGLINE, ) if feedline_r: self._ax.plot( @@ -735,8 +903,20 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic [ypos1, ypos2], color=self._style["lc"], linewidth=self._lwidth15, - zorder=PORDER_LINE, + zorder=PORDER_REGLINE, ) + # Mask off any lines or boxes in the bit label area to clean up + # from folding for ControlFlow and other wrapping gates + box = glob_data["patches_mod"].Rectangle( + xy=(glob_data["x_offset"] - 0.1, -fold_num * (glob_data["n_lines"] + 1) + 0.5), + width=-25.0, + height=-(fold_num + 1) * (glob_data["n_lines"] + 1), + fc=self._style["bg"], + ec=self._style["bg"], + linewidth=self._lwidth15, + zorder=PORDER_MASK, + ) + self._ax.add_patch(box) # draw index number if self._style["index"]: @@ -759,11 +939,45 @@ def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dic zorder=PORDER_TEXT, ) + def _add_nodes_and_coords( + self, nodes, node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data + ): + """Add the nodes from ControlFlowOps and their coordinates to the main circuit""" + for flow_drawers in self._flow_drawers.values(): + for i, flow_drawer in enumerate(flow_drawers): + nodes += flow_drawer._nodes + flow_drawer._get_coords( + node_data, + flow_drawer._flow_wire_map, + layer_widths, + qubits_dict, + clbits_dict, + glob_data, + flow_parent=flow_drawer._flow_parent, + is_not_first_block=(i > 0), + ) + # Recurse for ControlFlowOps inside the flow_drawer + flow_drawer._add_nodes_and_coords( + nodes, node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data + ) + def _draw_ops( - self, nodes, node_data, wire_map, layer_widths, clbits_dict, glob_data, verbose=False + self, + nodes, + node_data, + wire_map, + layer_widths, + qubits_dict, + clbits_dict, + glob_data, + verbose=False, ): """Draw the gates in the circuit""" + # Add the nodes from all the ControlFlowOps and their coordinates to the main nodes + self._add_nodes_and_coords( + nodes, node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data + ) prev_x_index = -1 for layer in nodes: l_width = [] @@ -779,18 +993,17 @@ def _draw_ops( print(op) # add conditional - if getattr(op, "condition", None): + if getattr(op, "condition", None) or isinstance(op, SwitchCaseOp): cond_xy = [ self._plot_coord( - curr_x_index, + node_data[node]["x_index"], clbits_dict[ii]["y"], layer_widths[node][0], glob_data, + isinstance(op, ControlFlowOp), ) for ii in clbits_dict ] - if clbits_dict: - curr_x_index = max(curr_x_index, glob_data["next_x_index"]) self._condition(node, node_data, wire_map, cond_xy, glob_data) # draw measure @@ -802,6 +1015,10 @@ def _draw_ops( if self._plot_barriers: self._barrier(node, node_data, glob_data) + # draw the box for control flow circuits + elif isinstance(op, ControlFlowOp): + self._flow_op_gate(node, node_data, glob_data) + # draw single qubit gates elif len(node_data[node]["q_xy"]) == 1 and not node.cargs: self._gate(node, node_data, glob_data) @@ -814,7 +1031,9 @@ def _draw_ops( else: self._multiqubit_gate(node, node_data, glob_data) - l_width.append(layer_widths[node][0]) + # Determine the max width of the circuit only at the top level + if not node_data[node]["inside_flow"]: + l_width.append(layer_widths[node][0]) # adjust the column if there have been barriers encountered, but not plotted barrier_offset = 0 @@ -823,8 +1042,7 @@ def _draw_ops( barrier_offset = ( -1 if all(getattr(nd.op, "_directive", False) for nd in layer) else 0 ) - - prev_x_index = curr_x_index + max(l_width) + barrier_offset - 1 + prev_x_index = curr_x_index + (max(l_width) if l_width else 0) + barrier_offset - 1 def _get_colors(self, node, node_data): """Get all the colors needed for drawing the circuit""" @@ -877,11 +1095,18 @@ def _get_colors(self, node, node_data): def _condition(self, node, node_data, wire_map, cond_xy, glob_data): """Add a conditional to a gate""" - label, val_bits = get_condition_label_val( - node.op.condition, self._circuit, self._cregbundle - ) - cond_bit_reg = node.op.condition[0] - cond_bit_val = int(node.op.condition[1]) + # For SwitchCaseOp convert the target to a fully closed Clbit or register + # in condition format + if isinstance(node.op, SwitchCaseOp): + if isinstance(node.op.target, Clbit): + condition = (node.op.target, 1) + else: + condition = (node.op.target, 2 ** (node.op.target.size) - 1) + else: + condition = node.op.condition + label, val_bits = get_condition_label_val(condition, self._circuit, self._cregbundle) + cond_bit_reg = condition[0] + cond_bit_val = int(condition[1]) first_clbit = len(self._qubits) cond_pos = [] @@ -926,6 +1151,11 @@ def _condition(self, node, node_data, wire_map, cond_xy, glob_data): qubit_b = min(node_data[node]["q_xy"], key=lambda xy: xy[1]) clbit_b = min(xy_plot, key=lambda xy: xy[1]) + # For IfElseOp, WhileLoopOp or SwitchCaseOp, place the condition + # at almost the left edge of the box + if isinstance(node.op, (IfElseOp, WhileLoopOp, SwitchCaseOp)): + qubit_b = (qubit_b[0], qubit_b[1] - (0.5 * HIG + 0.14)) + # display the label at the bottom of the lowest conditional and draw the double line xpos, ypos = clbit_b if isinstance(node.op, Measure): @@ -1028,7 +1258,7 @@ def _barrier(self, node, node_data, glob_data): ec=None, alpha=0.6, linewidth=self._lwidth15, - zorder=PORDER_GRAY, + zorder=PORDER_BARRIER, ) self._ax.add_patch(box) @@ -1118,7 +1348,6 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): ypos = min(ypos, cypos) wid = max(node_data[node]["width"] + 0.21, WID) - qubit_span = abs(ypos) - abs(ypos_max) height = HIG + qubit_span @@ -1187,6 +1416,161 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): zorder=PORDER_TEXT, ) + def _flow_op_gate(self, node, node_data, glob_data): + """Draw the box for a flow op circuit""" + xy = node_data[node]["q_xy"] + xpos = min(x[0] for x in xy) + ypos = min(y[1] for y in xy) + ypos_max = max(y[1] for y in xy) + + if_width = node_data[node]["width"][0] + WID + box_width = if_width + # Add the else and case widths to the if_width + for ewidth in node_data[node]["width"][1:]: + if ewidth > 0.0: + box_width += ewidth + WID + 0.3 + + qubit_span = abs(ypos) - abs(ypos_max) + height = HIG + qubit_span + + # Cycle through box colors based on depth. + # Default - blue, purple, green, black + colors = [ + self._style["dispcol"]["h"][0], + self._style["dispcol"]["u"][0], + self._style["dispcol"]["x"][0], + self._style["cc"], + ] + # To fold box onto next lines, draw it repeatedly, shifting + # it left by x_shift and down by y_shift + fold_level = 0 + end_x = xpos + box_width + + while end_x > 0.0: + x_shift = fold_level * self._fold + y_shift = fold_level * (glob_data["n_lines"] + 1) + end_x = xpos + box_width - x_shift + + # FancyBbox allows rounded corners + box = glob_data["patches_mod"].FancyBboxPatch( + xy=(xpos - x_shift, ypos - 0.5 * HIG - y_shift), + width=box_width, + height=height, + boxstyle="round, pad=0.1", + fc="none", + ec=colors[node_data[node]["nest_depth"] % 4], + linewidth=self._lwidth3, + zorder=PORDER_FLOW, + ) + self._ax.add_patch(box) + + if isinstance(node.op, IfElseOp): + flow_text = " If" + elif isinstance(node.op, WhileLoopOp): + flow_text = " While" + elif isinstance(node.op, ForLoopOp): + flow_text = " For" + elif isinstance(node.op, SwitchCaseOp): + flow_text = "Switch" + + self._ax.text( + xpos - x_shift - 0.08, + ypos_max + 0.2 - y_shift, + flow_text, + ha="left", + va="center", + fontsize=self._style["fs"], + color=node_data[node]["tc"], + clip_on=True, + zorder=PORDER_FLOW, + ) + if isinstance(node.op, ForLoopOp): + idx_set = str(node_data[node]["indexset"]) + # If a range was used display 'range' and grab the range value + # to be displayed below + if "range" in idx_set: + top_idx = " range" + idx_set = idx_set[6:-1] + self._ax.text( + xpos - x_shift - 0.08, + ypos_max - 0.2 - y_shift, + top_idx, + ha="left", + va="center", + fontsize=self._style["sfs"], + color=node_data[node]["tc"], + clip_on=True, + zorder=PORDER_FLOW, + ) + else: + # If a tuple, show first 4 elements followed by '...' + idx_set = str(node_data[node]["indexset"])[1:-1].split(",")[:5] + if len(idx_set) > 4: + idx_set[4] = "..." + idx_set = f"{', '.join(idx_set)}" + self._ax.text( + xpos - x_shift - 0.04, + ypos_max - 0.5 - y_shift, + idx_set, + ha="left", + va="center", + fontsize=self._style["sfs"], + color=node_data[node]["tc"], + clip_on=True, + zorder=PORDER_FLOW, + ) + # If there's an else or a case draw the vertical line and the name + else_case_text = "Else" if isinstance(node.op, IfElseOp) else "Case" + ewidth_incr = if_width + for case_num, ewidth in enumerate(node_data[node]["width"][1:]): + if ewidth > 0.0: + self._ax.plot( + [xpos + ewidth_incr + 0.3 - x_shift, xpos + ewidth_incr + 0.3 - x_shift], + [ypos - 0.5 * HIG - 0.08 - y_shift, ypos + height - 0.22 - y_shift], + color=colors[node_data[node]["nest_depth"] % 4], + linewidth=3.0, + linestyle="solid", + zorder=PORDER_FLOW, + ) + self._ax.text( + xpos + ewidth_incr + 0.4 - x_shift, + ypos_max + 0.2 - y_shift, + else_case_text, + ha="left", + va="center", + fontsize=self._style["fs"], + color=node_data[node]["tc"], + clip_on=True, + zorder=PORDER_FLOW, + ) + if isinstance(node.op, SwitchCaseOp): + jump_val = node_data[node]["jump_values"][case_num] + # If only one value, e.g. (0,) + if len(str(jump_val)) == 4: + jump_text = str(jump_val)[1] + elif "default" in str(jump_val): + jump_text = "default" + else: + # If a tuple, show first 4 elements followed by '...' + jump_text = str(jump_val)[1:-1].replace(" ", "").split(",")[:5] + if len(jump_text) > 4: + jump_text[4] = "..." + jump_text = f"{', '.join(jump_text)}" + self._ax.text( + xpos + ewidth_incr + 0.4 - x_shift, + ypos_max - 0.5 - y_shift, + jump_text, + ha="left", + va="center", + fontsize=self._style["sfs"], + color=node_data[node]["tc"], + clip_on=True, + zorder=PORDER_FLOW, + ) + ewidth_incr += ewidth + 1 + + fold_level += 1 + def _control_gate(self, node, node_data, glob_data): """Draw a controlled gate""" op = node.op @@ -1310,14 +1694,14 @@ def _x_tgt_qubit(self, xy, glob_data, ec=None, ac=None): [ypos - 0.2 * HIG, ypos + 0.2 * HIG], color=ac, linewidth=linewidth, - zorder=PORDER_GATE + 1, + zorder=PORDER_GATE_PLUS, ) self._ax.plot( [xpos - 0.2 * HIG, xpos + 0.2 * HIG], [ypos, ypos], color=ac, linewidth=linewidth, - zorder=PORDER_GATE + 1, + zorder=PORDER_GATE_PLUS, ) def _symmetric_gate(self, node, node_data, base_type, glob_data): @@ -1335,7 +1719,7 @@ def _symmetric_gate(self, node, node_data, base_type, glob_data): if not isinstance(op, ZGate) and isinstance(base_type, ZGate): num_ctrl_qubits = op.num_ctrl_qubits self._ctrl_qubit(xy[-1], glob_data, fc=ec, ec=ec, tc=tc) - self._line(qubit_b, qubit_t, lc=lc, zorder=PORDER_LINE + 1) + self._line(qubit_b, qubit_t, lc=lc, zorder=PORDER_LINE_PLUS) # cu1, cp, rzz, and controlled rzz gates (sidetext gates) elif isinstance(op, RZZGate) or isinstance(base_type, (U1Gate, PhaseGate, RZZGate)): @@ -1386,14 +1770,14 @@ def _swap_cross(self, xy, color=None): [ypos - 0.20 * WID, ypos + 0.20 * WID], color=color, linewidth=self._lwidth2, - zorder=PORDER_LINE + 1, + zorder=PORDER_LINE_PLUS, ) self._ax.plot( [xpos - 0.20 * WID, xpos + 0.20 * WID], [ypos + 0.20 * WID, ypos - 0.20 * WID], color=color, linewidth=self._lwidth2, - zorder=PORDER_LINE + 1, + zorder=PORDER_LINE_PLUS, ) def _sidetext(self, node, node_data, xy, tc=None, text=""): @@ -1451,18 +1835,23 @@ def _line(self, xy0, xy1, lc=None, ls=None, zorder=PORDER_LINE): zorder=zorder, ) - def _plot_coord(self, x_index, y_index, gate_width, glob_data): + def _plot_coord(self, x_index, y_index, gate_width, glob_data, flow_op=False): """Get the coord positions for an index""" - # check folding + + # Check folding fold = self._fold if self._fold > 0 else INFINITE_FOLD h_pos = x_index % fold + 1 - if h_pos + (gate_width - 1) > fold: + # Don't fold flow_ops here, only gates inside the flow_op + if not flow_op and h_pos + (gate_width - 1) > fold: x_index += fold - (h_pos - 1) x_pos = x_index % fold + glob_data["x_offset"] + 0.04 - x_pos += 0.5 * gate_width + if not flow_op: + x_pos += 0.5 * gate_width + else: + x_pos += 0.25 y_pos = y_index - (x_index // fold) * (glob_data["n_lines"] + 1) - # could have been updated, so need to store + # x_index could have been updated, so need to store glob_data["next_x_index"] = x_index return x_pos, y_pos diff --git a/releasenotes/notes/display-control-flow-mpl-drawer-2dbc7b57ac52d138.yaml b/releasenotes/notes/display-control-flow-mpl-drawer-2dbc7b57ac52d138.yaml new file mode 100644 index 000000000000..825dcdd63bfb --- /dev/null +++ b/releasenotes/notes/display-control-flow-mpl-drawer-2dbc7b57ac52d138.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + In :class:`~qiskit.visualization.circuit.MatplotlibDrawer`, operations + built from :class:`~qiskit.circuit.ControlFlowOp`, including + ``if``, ``else``, ``while``, ``for``, and ``switch/case``, whether + directly instantiated or built using methods in :class:`~qiskit.circuit.QuantumCircuit`, + will now fully display the circuits defined in the ControlFlowOps wrapped + with boxes to delineate the circuits. diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index 168905498abf..46ed46f19183 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -198,7 +198,7 @@ def test_reverse_bits(self): " ", ] ) - result = visualization.circuit_drawer(circuit, reverse_bits=True) + result = visualization.circuit_drawer(circuit, output="text", reverse_bits=True) self.assertEqual(result.__str__(), expected) def test_no_explict_cregbundle(self): diff --git a/test/visual/mpl/circuit/references/for_loop.png b/test/visual/mpl/circuit/references/for_loop.png new file mode 100644 index 000000000000..bc64f664114c Binary files /dev/null and b/test/visual/mpl/circuit/references/for_loop.png differ diff --git a/test/visual/mpl/circuit/references/if_else_body.png b/test/visual/mpl/circuit/references/if_else_body.png new file mode 100644 index 000000000000..a7a99c0264b4 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_body.png differ diff --git a/test/visual/mpl/circuit/references/if_else_op.png b/test/visual/mpl/circuit/references/if_else_op.png new file mode 100644 index 000000000000..6f363d8940d1 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_op.png differ diff --git a/test/visual/mpl/circuit/references/if_else_op_fold.png b/test/visual/mpl/circuit/references/if_else_op_fold.png new file mode 100644 index 000000000000..c9bec82cfcd3 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_op_fold.png differ diff --git a/test/visual/mpl/circuit/references/if_else_op_nested.png b/test/visual/mpl/circuit/references/if_else_op_nested.png new file mode 100644 index 000000000000..74781f8168a3 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_op_nested.png differ diff --git a/test/visual/mpl/circuit/references/if_else_op_textbook.png b/test/visual/mpl/circuit/references/if_else_op_textbook.png new file mode 100644 index 000000000000..2cd29cffca0c Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_op_textbook.png differ diff --git a/test/visual/mpl/circuit/references/if_else_op_wire_order.png b/test/visual/mpl/circuit/references/if_else_op_wire_order.png new file mode 100644 index 000000000000..6ee19c49f774 Binary files /dev/null and b/test/visual/mpl/circuit/references/if_else_op_wire_order.png differ diff --git a/test/visual/mpl/circuit/references/if_op.png b/test/visual/mpl/circuit/references/if_op.png new file mode 100644 index 000000000000..b5707626dbcc Binary files /dev/null and b/test/visual/mpl/circuit/references/if_op.png differ diff --git a/test/visual/mpl/circuit/references/switch_case.png b/test/visual/mpl/circuit/references/switch_case.png new file mode 100644 index 000000000000..2bc54e9c2082 Binary files /dev/null and b/test/visual/mpl/circuit/references/switch_case.png differ diff --git a/test/visual/mpl/circuit/references/while_loop.png b/test/visual/mpl/circuit/references/while_loop.png new file mode 100644 index 000000000000..9d9f93cb1a53 Binary files /dev/null and b/test/visual/mpl/circuit/references/while_loop.png differ diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index 77666d565818..3d83b41a964c 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -1573,7 +1573,322 @@ def test_barrier_label(self): circuit.barrier(label="End Y/X") fname = "barrier_label.png" - self.circuit_drawer(circuit, filename="barrier_label.png") + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_op(self): + """Test the IfElseOp with if only""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(2, "cr") + circuit = QuantumCircuit(qr, cr) + + with circuit.if_test((cr[1], 1)): + circuit.h(0) + circuit.cx(0, 1) + + fname = "if_op.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_op(self): + """Test the IfElseOp with else""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(2, "cr") + circuit = QuantumCircuit(qr, cr) + + with circuit.if_test((cr[1], 1)) as _else: + circuit.h(0) + circuit.cx(0, 1) + with _else: + circuit.cx(0, 1) + + fname = "if_else_op.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_op_textbook_style(self): + """Test the IfElseOp with else in textbook style""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(2, "cr") + circuit = QuantumCircuit(qr, cr) + + with circuit.if_test((cr[1], 1)) as _else: + circuit.h(0) + circuit.cx(0, 1) + with _else: + circuit.cx(0, 1) + + fname = "if_else_op_textbook.png" + self.circuit_drawer(circuit, style="textbook", filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_with_body(self): + """Test the IfElseOp with adding a body manually""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + circuit.h(0) + circuit.h(1) + circuit.measure(0, 1) + circuit.measure(1, 2) + circuit.x(2) + circuit.x(2, label="XLabel").c_if(cr, 2) + + qr2 = QuantumRegister(3, "qr2") + qc2 = QuantumCircuit(qr2, cr) + qc2.x(1) + qc2.y(1) + qc2.z(0) + qc2.x(0, label="X1i").c_if(cr, 4) + + circuit.if_else((cr[1], 1), qc2, None, [0, 1, 2], [0, 1, 2]) + circuit.x(0, label="X1i") + + fname = "if_else_body.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_op_nested(self): + """Test the IfElseOp with complex nested if/else""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + + circuit.h(0) + with circuit.if_test((cr[1], 1)) as _else: + circuit.x(0, label="X c_if").c_if(cr, 4) + with circuit.if_test((cr[2], 1)): + circuit.z(0) + circuit.y(1) + with circuit.if_test((cr[1], 1)): + circuit.y(1) + circuit.z(2) + with circuit.if_test((cr[2], 1)): + circuit.cx(0, 1) + with circuit.if_test((cr[1], 1)): + circuit.h(0) + circuit.x(1) + with _else: + circuit.y(1) + with circuit.if_test((cr[2], 1)): + circuit.x(0) + circuit.x(1) + inst = QuantumCircuit(2, 2, name="Inst").to_instruction() + circuit.append(inst, [qr[0], qr[1]], [cr[0], cr[1]]) + circuit.x(0) + + fname = "if_else_op_nested.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_op_wire_order(self): + """Test the IfElseOp with complex nested if/else and wire_order""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + + circuit.h(0) + with circuit.if_test((cr[1], 1)) as _else: + circuit.x(0, label="X c_if").c_if(cr, 4) + with circuit.if_test((cr[2], 1)): + circuit.z(0) + circuit.y(1) + with circuit.if_test((cr[1], 1)): + circuit.y(1) + circuit.z(2) + with circuit.if_test((cr[2], 1)): + circuit.cx(0, 1) + with circuit.if_test((cr[1], 1)): + circuit.h(0) + circuit.x(1) + with _else: + circuit.y(1) + with circuit.if_test((cr[2], 1)): + circuit.x(0) + circuit.x(1) + inst = QuantumCircuit(2, 2, name="Inst").to_instruction() + circuit.append(inst, [qr[0], qr[1]], [cr[0], cr[1]]) + circuit.x(0) + + fname = "if_else_op_wire_order.png" + self.circuit_drawer(circuit, wire_order=[2, 0, 3, 1, 4, 5, 6], filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_if_else_op_fold(self): + """Test the IfElseOp with complex nested if/else and fold""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + + circuit.h(0) + with circuit.if_test((cr[1], 1)) as _else: + circuit.x(0, label="X c_if").c_if(cr, 4) + with circuit.if_test((cr[2], 1)): + circuit.z(0) + circuit.y(1) + with circuit.if_test((cr[1], 1)): + circuit.y(1) + circuit.z(2) + with circuit.if_test((cr[2], 1)): + circuit.cx(0, 1) + with circuit.if_test((cr[1], 1)): + circuit.h(0) + circuit.x(1) + with _else: + circuit.y(1) + with circuit.if_test((cr[2], 1)): + circuit.x(0) + circuit.x(1) + inst = QuantumCircuit(2, 2, name="Inst").to_instruction() + circuit.append(inst, [qr[0], qr[1]], [cr[0], cr[1]]) + circuit.x(0) + + fname = "if_else_op_fold.png" + self.circuit_drawer(circuit, fold=7, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_while_loop_op(self): + """Test the WhileLoopOp""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + + circuit.h(0) + circuit.measure(0, 2) + with circuit.while_loop((cr[0], 0)): + circuit.h(0) + circuit.cx(0, 1) + circuit.measure(0, 0) + with circuit.if_test((cr[2], 1)): + circuit.x(0) + + fname = "while_loop.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_for_loop_op(self): + """Test the ForLoopOp""" + qr = QuantumRegister(4, "q") + cr = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qr, cr) + + a = Parameter("a") + circuit.h(0) + circuit.measure(0, 2) + with circuit.for_loop((2, 4, 8, 16), loop_parameter=a): + circuit.h(0) + circuit.cx(0, 1) + circuit.rx(pi / a, 1) + circuit.measure(0, 0) + with circuit.if_test((cr[2], 1)): + circuit.z(0) + + fname = "for_loop.png" + self.circuit_drawer(circuit, filename=fname) + + ratio = VisualTestUtilities._save_diff( + self._image_path(fname), + self._reference_path(fname), + fname, + FAILURE_DIFF_DIR, + FAILURE_PREFIX, + ) + self.assertGreaterEqual(ratio, 0.99) + + def test_switch_case_op(self): + """Test the SwitchCaseOp""" + qreg = QuantumRegister(3, "q") + creg = ClassicalRegister(3, "cr") + circuit = QuantumCircuit(qreg, creg) + + circuit.h([0, 1, 2]) + circuit.measure([0, 1, 2], [0, 1, 2]) + + with circuit.switch(creg) as case: + with case(0, 1, 2): + circuit.x(0) + with case(3, 4, 5): + circuit.y(1) + circuit.y(0) + circuit.y(0) + with case(case.DEFAULT): + circuit.cx(0, 1) + circuit.h(0) + + fname = "switch_case.png" + self.circuit_drawer(circuit, filename=fname) ratio = VisualTestUtilities._save_diff( self._image_path(fname), diff --git a/test/visual/mpl/graph/references/state_city.png b/test/visual/mpl/graph/references/state_city.png index 52efe9911a8b..e8351c01b510 100644 Binary files a/test/visual/mpl/graph/references/state_city.png and b/test/visual/mpl/graph/references/state_city.png differ