From cab90ca64ed7574441f229df52e658879bbe1d85 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Mon, 24 Jul 2023 03:59:46 -0700 Subject: [PATCH 01/19] Switch to using NodeData class (#10478) --- qiskit/visualization/circuit/matplotlib.py | 239 +++++++++++---------- 1 file changed, 129 insertions(+), 110 deletions(-) diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 983311f067c8..2c64ad0e4a47 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -294,9 +294,7 @@ 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", "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 per node filled with class NodeData attributes node_data = {} # dicts for the names and locations of register/bit labels @@ -412,23 +410,23 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): layer_widths[node] = [1, layer_num, self._flow_parent] op = node.op - node_data[node] = {} - node_data[node]["width"] = WID + node_data[node] = NodeData() + node_data[node].width = WID num_ctrl_qubits = 0 if not hasattr(op, "num_ctrl_qubits") else op.num_ctrl_qubits if ( getattr(op, "_directive", False) and (not op.label or not self._plot_barriers) ) or isinstance(op, Measure): - node_data[node]["raw_gate_text"] = op.name + node_data[node].raw_gate_text = op.name continue base_type = None if not hasattr(op, "base_gate") else op.base_gate gate_text, ctrl_text, raw_gate_text = get_gate_ctrl_text( op, "mpl", style=self._style, calibrations=self._calibrations ) - node_data[node]["gate_text"] = gate_text - node_data[node]["ctrl_text"] = ctrl_text - node_data[node]["raw_gate_text"] = raw_gate_text - node_data[node]["param_text"] = "" + node_data[node].gate_text = gate_text + node_data[node].ctrl_text = ctrl_text + node_data[node].raw_gate_text = raw_gate_text + node_data[node].param_text = "" # if single qubit, no params, and no labels, layer_width is 1 if ( @@ -457,7 +455,7 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): param_text = get_param_str(op, "mpl", ndigits=3) if isinstance(op, Initialize): param_text = f"$[{param_text.replace('$', '')}]$" - node_data[node]["param_text"] = param_text + node_data[node].param_text = param_text raw_param_width = self._get_text_width( param_text, glob_data, fontsize=self._style["sfs"], param=True ) @@ -480,8 +478,8 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): # 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 + 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 @@ -490,15 +488,15 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): # 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] + node_data[node].indexset = op.params[0] elif isinstance(op, SwitchCaseOp): - node_data[node]["jump_values"] = [] + 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) + node_data[node].jump_values.append(jump_values) # Now process the circuits inside the ControlFlowOps for circ_num, circuit in enumerate(circuit_list): @@ -506,9 +504,7 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): # 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 - ) + 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] @@ -547,7 +543,7 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): for flow_layer in nodes: for flow_node in flow_layer: if isinstance(node.op, SwitchCaseOp): - node_data[flow_node]["case_num"] = circ_num + 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 @@ -561,7 +557,7 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): # 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) + node_data[node].width.append(raw_gate_width) # Otherwise, standard gate or multiqubit gate else: @@ -577,7 +573,7 @@ def _get_layer_widths(self, node_data, wire_map, glob_data): if box_width > widest_box: widest_box = box_width if not isinstance(node.op, ControlFlowOp): - node_data[node]["width"] = max(raw_gate_width, raw_param_width) + node_data[node].width = max(raw_gate_width, raw_param_width) for node in layer: layer_widths[node][0] = int(widest_box) + 1 @@ -682,19 +678,15 @@ def _get_coords( # 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 - ) + 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 + 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 + if 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 = [] @@ -714,14 +706,14 @@ def _get_coords( 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"] + node_data[node].inside_flow = True + x_index = node_data[node].x_index else: - node_data[node]["inside_flow"] = False + node_data[node].inside_flow = False x_index = curr_x_index # qubit coordinates - node_data[node]["q_xy"] = [ + node_data[node].q_xy = [ self._plot_coord( x_index, qubits_dict[ii]["y"], @@ -732,7 +724,7 @@ def _get_coords( for ii in q_indxs ] # clbit coordinates - node_data[node]["c_xy"] = [ + node_data[node].c_xy = [ self._plot_coord( x_index, clbits_dict[ii]["y"], @@ -747,7 +739,7 @@ def _get_coords( 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 + node_data[node].x_index = x_index # adjust the column if there have been barriers encountered, but not plotted barrier_offset = 0 @@ -996,7 +988,7 @@ def _draw_ops( if getattr(op, "condition", None) or isinstance(op, SwitchCaseOp): cond_xy = [ self._plot_coord( - node_data[node]["x_index"], + node_data[node].x_index, clbits_dict[ii]["y"], layer_widths[node][0], glob_data, @@ -1020,7 +1012,7 @@ def _draw_ops( 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: + elif len(node_data[node].q_xy) == 1 and not node.cargs: self._gate(node, node_data, glob_data) # draw controlled gates @@ -1032,7 +1024,7 @@ def _draw_ops( self._multiqubit_gate(node, node_data, glob_data) # Determine the max width of the circuit only at the top level - if not node_data[node]["inside_flow"]: + 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 @@ -1050,8 +1042,8 @@ def _get_colors(self, node, node_data): op = node.op base_name = None if not hasattr(op, "base_gate") else op.base_gate.name color = None - if node_data[node]["raw_gate_text"] in self._style["dispcol"]: - color = self._style["dispcol"][node_data[node]["raw_gate_text"]] + if node_data[node].raw_gate_text in self._style["dispcol"]: + color = self._style["dispcol"][node_data[node].raw_gate_text] elif op.name in self._style["dispcol"]: color = self._style["dispcol"][op.name] if color is not None: @@ -1085,12 +1077,12 @@ def _get_colors(self, node, node_data): lc = fc # Subtext needs to be same color as gate text sc = gt - node_data[node]["fc"] = fc - node_data[node]["ec"] = ec - node_data[node]["gt"] = gt - node_data[node]["tc"] = self._style["tc"] - node_data[node]["sc"] = sc - node_data[node]["lc"] = lc + node_data[node].fc = fc + node_data[node].ec = ec + node_data[node].gt = gt + node_data[node].tc = self._style["tc"] + node_data[node].sc = sc + node_data[node].lc = lc def _condition(self, node, node_data, wire_map, cond_xy, glob_data): """Add a conditional to a gate""" @@ -1148,7 +1140,7 @@ def _condition(self, node, node_data, wire_map, cond_xy, glob_data): self._ax.add_patch(box) xy_plot.append(xy) - qubit_b = min(node_data[node]["q_xy"], key=lambda xy: xy[1]) + 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 @@ -1175,8 +1167,8 @@ def _condition(self, node, node_data, wire_map, cond_xy, glob_data): def _measure(self, node, node_data, glob_data): """Draw the measure symbol and the line to the clbit""" - qx, qy = node_data[node]["q_xy"][0] - cx, cy = node_data[node]["c_xy"][0] + qx, qy = node_data[node].q_xy[0] + cx, cy = node_data[node].c_xy[0] register, _, reg_index = get_bit_reg_index(self._circuit, node.cargs[0]) # draw gate box @@ -1190,7 +1182,7 @@ def _measure(self, node, node_data, glob_data): theta1=0, theta2=180, fill=False, - ec=node_data[node]["gt"], + ec=node_data[node].gt, linewidth=self._lwidth2, zorder=PORDER_GATE, ) @@ -1198,13 +1190,13 @@ def _measure(self, node, node_data, glob_data): self._ax.plot( [qx, qx + 0.35 * WID], [qy - 0.15 * HIG, qy + 0.20 * HIG], - color=node_data[node]["gt"], + color=node_data[node].gt, linewidth=self._lwidth2, zorder=PORDER_GATE, ) # arrow self._line( - node_data[node]["q_xy"][0], + node_data[node].q_xy[0], [cx, cy + 0.35 * WID], lc=self._style["cc"], ls=self._style["cline"], @@ -1235,7 +1227,7 @@ def _measure(self, node, node_data, glob_data): def _barrier(self, node, node_data, glob_data): """Draw a barrier""" - for i, xy in enumerate(node_data[node]["q_xy"]): + for i, xy in enumerate(node_data[node].q_xy): xpos, ypos = xy # For the topmost barrier, reduce the rectangle if there's a label to allow for the text. if i == 0 and node.op.label is not None: @@ -1272,7 +1264,7 @@ def _barrier(self, node, node_data, glob_data): ha="center", va="top", fontsize=self._style["fs"], - color=node_data[node]["tc"], + color=node_data[node].tc, clip_on=True, zorder=PORDER_TEXT, ) @@ -1280,44 +1272,44 @@ def _barrier(self, node, node_data, glob_data): def _gate(self, node, node_data, glob_data, xy=None): """Draw a 1-qubit gate""" if xy is None: - xy = node_data[node]["q_xy"][0] + xy = node_data[node].q_xy[0] xpos, ypos = xy - wid = max(node_data[node]["width"], WID) + wid = max(node_data[node].width, WID) box = glob_data["patches_mod"].Rectangle( xy=(xpos - 0.5 * wid, ypos - 0.5 * HIG), width=wid, height=HIG, - fc=node_data[node]["fc"], - ec=node_data[node]["ec"], + fc=node_data[node].fc, + ec=node_data[node].ec, linewidth=self._lwidth15, zorder=PORDER_GATE, ) self._ax.add_patch(box) - if "gate_text" in node_data[node]: + if node_data[node].gate_text: gate_ypos = ypos - if "param_text" in node_data[node] and node_data[node]["param_text"] != "": + if node_data[node].param_text: gate_ypos = ypos + 0.15 * HIG self._ax.text( xpos, ypos - 0.3 * HIG, - node_data[node]["param_text"], + node_data[node].param_text, ha="center", va="center", fontsize=self._style["sfs"], - color=node_data[node]["sc"], + color=node_data[node].sc, clip_on=True, zorder=PORDER_TEXT, ) self._ax.text( xpos, gate_ypos, - node_data[node]["gate_text"], + node_data[node].gate_text, ha="center", va="center", fontsize=self._style["fs"], - color=node_data[node]["gt"], + color=node_data[node].gt, clip_on=True, zorder=PORDER_TEXT, ) @@ -1326,11 +1318,11 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): """Draw a gate covering more than one qubit""" op = node.op if xy is None: - xy = node_data[node]["q_xy"] + xy = node_data[node].q_xy # Swap gate if isinstance(op, SwapGate): - self._swap(xy, node, node_data, node_data[node]["lc"]) + self._swap(xy, node, node_data, node_data[node].lc) return # RZZ Gate @@ -1338,7 +1330,7 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): self._symmetric_gate(node, node_data, RZZGate, glob_data) return - c_xy = node_data[node]["c_xy"] + c_xy = node_data[node].c_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) @@ -1347,7 +1339,7 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): cypos = min(y[1] for y in c_xy) ypos = min(ypos, cypos) - wid = max(node_data[node]["width"] + 0.21, WID) + wid = max(node_data[node].width + 0.21, WID) qubit_span = abs(ypos) - abs(ypos_max) height = HIG + qubit_span @@ -1355,8 +1347,8 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): xy=(xpos - 0.5 * wid, ypos - 0.5 * HIG), width=wid, height=height, - fc=node_data[node]["fc"], - ec=node_data[node]["ec"], + fc=node_data[node].fc, + ec=node_data[node].ec, linewidth=self._lwidth15, zorder=PORDER_GATE, ) @@ -1371,7 +1363,7 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): ha="left", va="center", fontsize=self._style["fs"], - color=node_data[node]["gt"], + color=node_data[node].gt, clip_on=True, zorder=PORDER_TEXT, ) @@ -1385,48 +1377,48 @@ def _multiqubit_gate(self, node, node_data, glob_data, xy=None): ha="left", va="center", fontsize=self._style["fs"], - color=node_data[node]["gt"], + color=node_data[node].gt, clip_on=True, zorder=PORDER_TEXT, ) - if "gate_text" in node_data[node] and node_data[node]["gate_text"] != "": + if node_data[node].gate_text: gate_ypos = ypos + 0.5 * qubit_span - if "param_text" in node_data[node] and node_data[node]["param_text"] != "": + if node_data[node].param_text: gate_ypos = ypos + 0.4 * height self._ax.text( xpos + 0.11, ypos + 0.2 * height, - node_data[node]["param_text"], + node_data[node].param_text, ha="center", va="center", fontsize=self._style["sfs"], - color=node_data[node]["sc"], + color=node_data[node].sc, clip_on=True, zorder=PORDER_TEXT, ) self._ax.text( xpos + 0.11, gate_ypos, - node_data[node]["gate_text"], + node_data[node].gate_text, ha="center", va="center", fontsize=self._style["fs"], - color=node_data[node]["gt"], + color=node_data[node].gt, clip_on=True, 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"] + 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 + 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:]: + for ewidth in node_data[node].width[1:]: if ewidth > 0.0: box_width += ewidth + WID + 0.3 @@ -1458,7 +1450,7 @@ def _flow_op_gate(self, node, node_data, glob_data): height=height, boxstyle="round, pad=0.1", fc="none", - ec=colors[node_data[node]["nest_depth"] % 4], + ec=colors[node_data[node].nest_depth % 4], linewidth=self._lwidth3, zorder=PORDER_FLOW, ) @@ -1480,12 +1472,12 @@ def _flow_op_gate(self, node, node_data, glob_data): ha="left", va="center", fontsize=self._style["fs"], - color=node_data[node]["tc"], + color=node_data[node].tc, clip_on=True, zorder=PORDER_FLOW, ) if isinstance(node.op, ForLoopOp): - idx_set = str(node_data[node]["indexset"]) + 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: @@ -1498,13 +1490,13 @@ def _flow_op_gate(self, node, node_data, glob_data): ha="left", va="center", fontsize=self._style["sfs"], - color=node_data[node]["tc"], + 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] + 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)}" @@ -1515,19 +1507,19 @@ def _flow_op_gate(self, node, node_data, glob_data): ha="left", va="center", fontsize=self._style["sfs"], - color=node_data[node]["tc"], + 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:]): + 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], + color=colors[node_data[node].nest_depth % 4], linewidth=3.0, linestyle="solid", zorder=PORDER_FLOW, @@ -1539,12 +1531,12 @@ def _flow_op_gate(self, node, node_data, glob_data): ha="left", va="center", fontsize=self._style["fs"], - color=node_data[node]["tc"], + 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] + 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] @@ -1563,7 +1555,7 @@ def _flow_op_gate(self, node, node_data, glob_data): ha="left", va="center", fontsize=self._style["sfs"], - color=node_data[node]["tc"], + color=node_data[node].tc, clip_on=True, zorder=PORDER_FLOW, ) @@ -1574,7 +1566,7 @@ def _flow_op_gate(self, node, node_data, glob_data): def _control_gate(self, node, node_data, glob_data): """Draw a controlled gate""" op = node.op - xy = node_data[node]["q_xy"] + xy = node_data[node].q_xy base_type = None if not hasattr(op, "base_gate") else op.base_gate qubit_b = min(xy, key=lambda xy: xy[1]) qubit_t = max(xy, key=lambda xy: xy[1]) @@ -1585,12 +1577,12 @@ def _control_gate(self, node, node_data, glob_data): num_ctrl_qubits, xy, glob_data, - ec=node_data[node]["ec"], - tc=node_data[node]["tc"], - text=node_data[node]["ctrl_text"], + ec=node_data[node].ec, + tc=node_data[node].tc, + text=node_data[node].ctrl_text, qargs=node.qargs, ) - self._line(qubit_b, qubit_t, lc=node_data[node]["lc"]) + self._line(qubit_b, qubit_t, lc=node_data[node].lc) if isinstance(op, RZZGate) or isinstance(base_type, (U1Gate, PhaseGate, ZGate, RZZGate)): self._symmetric_gate(node, node_data, base_type, glob_data) @@ -1598,13 +1590,13 @@ def _control_gate(self, node, node_data, glob_data): elif num_qargs == 1 and isinstance(base_type, XGate): tgt_color = self._style["dispcol"]["target"] tgt = tgt_color if isinstance(tgt_color, str) else tgt_color[0] - self._x_tgt_qubit(xy[num_ctrl_qubits], glob_data, ec=node_data[node]["ec"], ac=tgt) + self._x_tgt_qubit(xy[num_ctrl_qubits], glob_data, ec=node_data[node].ec, ac=tgt) elif num_qargs == 1: self._gate(node, node_data, glob_data, xy[num_ctrl_qubits:][0]) elif isinstance(base_type, SwapGate): - self._swap(xy[num_ctrl_qubits:], node, node_data, node_data[node]["lc"]) + self._swap(xy[num_ctrl_qubits:], node, node_data, node_data[node].lc) else: self._multiqubit_gate(node, node_data, glob_data, xy[num_ctrl_qubits:]) @@ -1707,13 +1699,13 @@ def _x_tgt_qubit(self, xy, glob_data, ec=None, ac=None): def _symmetric_gate(self, node, node_data, base_type, glob_data): """Draw symmetric gates for cz, cu1, cp, and rzz""" op = node.op - xy = node_data[node]["q_xy"] + xy = node_data[node].q_xy qubit_b = min(xy, key=lambda xy: xy[1]) qubit_t = max(xy, key=lambda xy: xy[1]) base_type = None if not hasattr(op, "base_gate") else op.base_gate - ec = node_data[node]["ec"] - tc = node_data[node]["tc"] - lc = node_data[node]["lc"] + ec = node_data[node].ec + tc = node_data[node].tc + lc = node_data[node].lc # cz and mcz gates if not isinstance(op, ZGate) and isinstance(base_type, ZGate): @@ -1724,7 +1716,7 @@ def _symmetric_gate(self, node, node_data, base_type, glob_data): # cu1, cp, rzz, and controlled rzz gates (sidetext gates) elif isinstance(op, RZZGate) or isinstance(base_type, (U1Gate, PhaseGate, RZZGate)): num_ctrl_qubits = 0 if isinstance(op, RZZGate) else op.num_ctrl_qubits - gate_text = "P" if isinstance(base_type, PhaseGate) else node_data[node]["gate_text"] + gate_text = "P" if isinstance(base_type, PhaseGate) else node_data[node].gate_text self._ctrl_qubit(xy[num_ctrl_qubits], glob_data, fc=ec, ec=ec, tc=tc) if not isinstance(base_type, (U1Gate, PhaseGate)): @@ -1735,7 +1727,7 @@ def _symmetric_gate(self, node, node_data, base_type, glob_data): node_data, qubit_b, tc=tc, - text=f"{gate_text} ({node_data[node]['param_text']})", + text=f"{gate_text} ({node_data[node].param_text})", ) self._line(qubit_b, qubit_t, lc=lc) @@ -1746,8 +1738,8 @@ def _swap(self, xy, node, node_data, color=None): self._line(xy[0], xy[1], lc=color) # add calibration text - gate_text = node_data[node]["gate_text"].split("\n")[-1] - if node_data[node]["raw_gate_text"] in self._calibrations: + gate_text = node_data[node].gate_text.split("\n")[-1] + if node_data[node].raw_gate_text in self._calibrations: xpos, ypos = xy[0] self._ax.text( xpos, @@ -1785,7 +1777,7 @@ def _sidetext(self, node, node_data, xy, tc=None, text=""): xpos, ypos = xy # 0.11 = the initial gap, add 1/2 text width to place on the right - xp = xpos + 0.11 + node_data[node]["width"] / 2 + xp = xpos + 0.11 + node_data[node].width / 2 self._ax.text( xp, ypos + HIG, @@ -1855,3 +1847,30 @@ def _plot_coord(self, x_index, y_index, gate_width, glob_data, flow_op=False): # x_index could have been updated, so need to store glob_data["next_x_index"] = x_index return x_pos, y_pos + + +class NodeData: + """Class containing drawing data on a per node basis""" + + def __init__(self): + # Node data for positioning + self.width = 0.0 + self.x_index = 0 + self.q_xy = [] + self.c_xy = [] + + # Node data for text + self.gate_text = "" + self.raw_gate_text = "" + self.ctrl_text = "" + self.param_text = "" + + # Node data for color + self.fc = self.ec = self.lc = self.sc = self.gt = self.tc = 0 + + # Special values stored for ControlFlowOps + self.nest_depth = 0 + self.inside_flow = False + self.indexset = () # List of indices used for ForLoopOp + self.jump_values = [] # List of jump values used for SwitchCaseOp + self.case_num = 0 # Used for SwitchCaseOp From 208c29a649d9cce24b2c175115f6a15474c7d438 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 24 Jul 2023 13:05:38 +0200 Subject: [PATCH 02/19] raise an exception with a custom gate with clbits or no qubits (#10438) * raise an exception with a custom gate with no qubits or with one-or-more clbits * QASM2ExportError * test_circuit_raises_invalid_custom_gate_1 * test_circuit_raises_invalid_custom_gate_2 * reno * Split error messages to be more specific * Fix testing bug * Fixup release note --------- Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 15 +++++++++++- ...d_custom_instruction-7738db7ba1a1a5cf.yaml | 8 +++++++ test/python/circuit/test_circuit_qasm.py | 24 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/qasm_invalid_custom_instruction-7738db7ba1a1a5cf.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 5dfea905bdc3..8727d78b810e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -5025,7 +5025,9 @@ def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_defi Returns a potentially new :class:`.Instruction`, which should be used for the :meth:`~.Instruction.qasm` call (it may have been renamed).""" - from qiskit.circuit import library as lib # pylint: disable=cyclic-import + # pylint: disable=cyclic-import + from qiskit.circuit import library as lib + from qiskit.qasm2 import QASM2ExportError if operation.name in existing_gate_names: return operation @@ -5086,6 +5088,17 @@ def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_defi ) else: parameters_qasm = "" + + if operation.num_qubits == 0: + raise QASM2ExportError( + f"OpenQASM 2 cannot represent '{operation.name}, which acts on zero qubits." + ) + if operation.num_clbits != 0: + raise QASM2ExportError( + f"OpenQASM 2 cannot represent '{operation.name}', which acts on {operation.num_clbits}" + " classical bits." + ) + qubits_qasm = ",".join(f"q{i}" for i in range(parameterized_operation.num_qubits)) parameterized_definition = getattr(parameterized_operation, "definition", None) if parameterized_definition is None: diff --git a/releasenotes/notes/qasm_invalid_custom_instruction-7738db7ba1a1a5cf.yaml b/releasenotes/notes/qasm_invalid_custom_instruction-7738db7ba1a1a5cf.yaml new file mode 100644 index 000000000000..545032c17a17 --- /dev/null +++ b/releasenotes/notes/qasm_invalid_custom_instruction-7738db7ba1a1a5cf.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Qiskit can represent custom instructions that act on zero qubits, or on a non-zero number of + classical bits. These cannot be exported to OpenQASM 2, but previously :meth:`.QuantumCircuit.qasm` + would try, and output invalid OpenQASM 2. Instead, a :exc:`.QASM2ExportError` will now correctly + be raised. See `#7351 `__ and + `#10435 `__. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 32f12e2c89ef..83e1d7999a47 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -657,6 +657,30 @@ def test_circuit_raises_on_single_bit_condition(self): with self.assertRaisesRegex(QasmError, "OpenQASM 2 can only condition on registers"): qc.qasm() + def test_circuit_raises_invalid_custom_gate_no_qubits(self): + """OpenQASM 2 exporter of custom gates with no qubits. + See: https://github.com/Qiskit/qiskit-terra/issues/10435""" + legit_circuit = QuantumCircuit(5, name="legit_circuit") + empty_circuit = QuantumCircuit(name="empty_circuit") + legit_circuit.append(empty_circuit) + + with self.assertRaisesRegex(QasmError, "acts on zero qubits"): + legit_circuit.qasm() + + def test_circuit_raises_invalid_custom_gate_clbits(self): + """OpenQASM 2 exporter of custom instruction. + See: https://github.com/Qiskit/qiskit-terra/issues/7351""" + instruction = QuantumCircuit(2, 2, name="inst") + instruction.cx(0, 1) + instruction.measure([0, 1], [0, 1]) + custom_instruction = instruction.to_instruction() + + qc = QuantumCircuit(2, 2) + qc.append(custom_instruction, [0, 1], [0, 1]) + + with self.assertRaisesRegex(QasmError, "acts on 2 classical bits"): + qc.qasm() + def test_circuit_qasm_with_permutations(self): """Test circuit qasm() method with Permutation gates.""" From 802a735ebea547d0d96339c2de4a10f04b0ab8a6 Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:32:04 +0300 Subject: [PATCH 03/19] Fix Pulse channel index validation (#10476) * Correct channel index validation * Add tests and release note. --- qiskit/pulse/channels.py | 2 +- ...l-validation-bug-fix-c06f8445cecc8478.yaml | 5 +++ test/python/pulse/test_channels.py | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/channel-validation-bug-fix-c06f8445cecc8478.yaml diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index c88fddadd77b..687b837700d3 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -121,7 +121,7 @@ def _validate_index(self, index: Any) -> None: if index.is_integer(): index = int(index) - if not isinstance(index, (int, np.integer)) and index < 0: + if not isinstance(index, (int, np.integer)) or index < 0: raise PulseError("Channel index must be a nonnegative integer") @property diff --git a/releasenotes/notes/channel-validation-bug-fix-c06f8445cecc8478.yaml b/releasenotes/notes/channel-validation-bug-fix-c06f8445cecc8478.yaml new file mode 100644 index 000000000000..b68ae2a68dfe --- /dev/null +++ b/releasenotes/notes/channel-validation-bug-fix-c06f8445cecc8478.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :class:`.pulse.Channel` where index validation was done incorrectly and only + raised an error when the index was both non-integer and negative, instead of either. diff --git a/test/python/pulse/test_channels.py b/test/python/pulse/test_channels.py index 604202fb5754..2ad0d08fe259 100644 --- a/test/python/pulse/test_channels.py +++ b/test/python/pulse/test_channels.py @@ -25,6 +25,7 @@ PulseChannel, RegisterSlot, SnapshotChannel, + PulseError, ) from qiskit.test import QiskitTestCase @@ -88,6 +89,13 @@ def test_default(self): self.assertEqual(memory_slot.name, "m123") self.assertTrue(isinstance(memory_slot, ClassicalIOChannel)) + def test_validation(self): + """Test channel validation""" + with self.assertRaises(PulseError): + MemorySlot(0.5) + with self.assertRaises(PulseError): + MemorySlot(-1) + class TestRegisterSlot(QiskitTestCase): """RegisterSlot tests.""" @@ -100,6 +108,13 @@ def test_default(self): self.assertEqual(register_slot.name, "c123") self.assertTrue(isinstance(register_slot, ClassicalIOChannel)) + def test_validation(self): + """Test channel validation""" + with self.assertRaises(PulseError): + RegisterSlot(0.5) + with self.assertRaises(PulseError): + RegisterSlot(-1) + class TestSnapshotChannel(QiskitTestCase): """SnapshotChannel tests.""" @@ -123,6 +138,13 @@ def test_default(self): self.assertEqual(drive_channel.index, 123) self.assertEqual(drive_channel.name, "d123") + def test_validation(self): + """Test channel validation""" + with self.assertRaises(PulseError): + DriveChannel(0.5) + with self.assertRaises(PulseError): + DriveChannel(-1) + class TestControlChannel(QiskitTestCase): """ControlChannel tests.""" @@ -134,6 +156,13 @@ def test_default(self): self.assertEqual(control_channel.index, 123) self.assertEqual(control_channel.name, "u123") + def test_validation(self): + """Test channel validation""" + with self.assertRaises(PulseError): + ControlChannel(0.5) + with self.assertRaises(PulseError): + ControlChannel(-1) + class TestMeasureChannel(QiskitTestCase): """MeasureChannel tests.""" @@ -145,6 +174,13 @@ def test_default(self): self.assertEqual(measure_channel.index, 123) self.assertEqual(measure_channel.name, "m123") + def test_validation(self): + """Test channel validation""" + with self.assertRaises(PulseError): + MeasureChannel(0.5) + with self.assertRaises(PulseError): + MeasureChannel(-1) + if __name__ == "__main__": unittest.main() From 845746daff2b9bc8c7e66b9ca205b888d5224d92 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 24 Jul 2023 20:25:13 +0100 Subject: [PATCH 04/19] Fix documentation of `SwitchCaseOp` (#10484) The class page for the `SwitchCaseOp` had some broken hyperlinks, and the documentation of the switch `CASE_DEFAULT` object was broken. This fixes both, and adds some more explicit examples of usage of the `CASE_DEFAULT` object, with some overlap of what was already in `QuantumCircuit.switch`. --- qiskit/circuit/__init__.py | 38 +++++++++++++++++++++-- qiskit/circuit/controlflow/switch_case.py | 7 ++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index d319cabc418c..7c0548db797a 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -296,9 +296,41 @@ The :class:`.SwitchCaseOp` also understands a special value: -.. py:data: CASE_DEFAULT - Used as a possible "label" in a :class:`.SwitchCaseOp` to represent the default case. This will - always match, if it is tried. +.. py:data:: CASE_DEFAULT + + A special object that represents the "default" case of a switch statement. If you use this as a + case target, it must be the last case, and will match anything that wasn't already matched. For + example:: + + from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister + from qiskit.circuit import SwitchCaseOp, CASE_DEFAULT + + body0 = QuantumCircuit(2, 2) + body0.x(0) + body1 = QuantumCircuit(2, 2) + body1.z(0) + body2 = QuantumCircuit(2, 2) + body2.cx(0, 1) + + qr, cr = QuantumRegister(2), ClassicalRegister(2) + qc = QuantumCircuit(qr, cr) + qc.switch(cr, [(0, body0), (1, body1), (CASE_DEFAULT, body2)], qr, cr) + + When using the builder interface of :meth:`.QuantumCircuit.switch`, this can also be accessed as + the ``DEFAULT`` attribute of the bound case-builder object, such as:: + + from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister + + qr, cr = QuantumRegister(2), ClassicalRegister(2) + qc = QuantumCircuit(qr, cr) + with qc.switch(cr) as case: + with case(0): + qc.x(0) + with case(1): + qc.z(0) + with case(case.DEFAULT): + qc.cx(0, 1) + Parametric Quantum Circuits --------------------------- diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 5fc4aac27bb9..0f215a9bcbb8 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -41,8 +41,7 @@ def __repr__(self): """A special object that represents the "default" case of a switch statement. If you use this as a case target, it must be the last case, and will match anything that wasn't already matched. When using the builder interface of :meth:`.QuantumCircuit.switch`, this can also be accessed as the -``DEFAULT`` attribute of the bound case-builder object. -""" +``DEFAULT`` attribute of the bound case-builder object.""" class SwitchCaseOp(ControlFlowOp): @@ -51,12 +50,12 @@ class SwitchCaseOp(ControlFlowOp): be used to represent a default condition. This is the low-level interface for creating a switch-case statement; in general, the circuit - method :meth:`.QuantumCircuit.switch_case` should be used as a context manager to access the + method :meth:`.QuantumCircuit.switch` should be used as a context manager to access the builder interface. At the low level, you must ensure that all the circuit blocks contain equal numbers of qubits and clbits, and that the order the virtual bits of the containing circuit should be bound is the same for all blocks. This will likely mean that each circuit block is wider than its natural width, as each block must span the union of all the spaces covered by - _any_ of the blocks. + *any* of the blocks. Args: target: the runtime value to switch on. From e75893d455577b3d211f85b84bb93f69e54b435f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 24 Jul 2023 21:04:05 +0100 Subject: [PATCH 05/19] Fix empty-barrier handling in OpenQASM 2 (#10469) The new parser would allow a `barrier;` statement, implicitly broadcasting it across all qubits in scope. This is technically not supported by the OpenQASM 2 specification, but is a useful quality-of-life extension to the specification (in the same way that Qiskit interprets barriers, and the OpenQASM 3 specification defines the `barrier;` statement). The precise rule is added to the new parser's `strict` mode. The OpenQASM 2 _exporter_ similarly should not have been putting out `barrier;` statements. These could only occur in Qiskit when a barrier was explicitly constructed with zero elements (as opposed to the call `QuantumCircuit.barrier()`, which has the all-in-scope behaviour), and consequently have no actual meaning or effect. The exporter is modified to simply skip such instructions, for as long as Qiskit permits the qubitless barrier statement. --- crates/qasm2/src/parse.rs | 9 +++++++++ qiskit/circuit/quantumcircuit.py | 4 ++++ ...m2-fix-zero-op-barrier-4af211b119d5b24d.yaml | 13 +++++++++++++ test/python/circuit/test_circuit_qasm.py | 17 +++++++++++++++++ test/python/qasm2/test_parse_errors.py | 7 +++++++ 5 files changed, 50 insertions(+) create mode 100644 releasenotes/notes/qasm2-fix-zero-op-barrier-4af211b119d5b24d.yaml diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index 9960258074a0..e4c749841124 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -1351,6 +1351,15 @@ impl State { } self.check_trailing_comma(comma.as_ref())?; qubits + } else if self.strict { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + barrier_token.line, + barrier_token.col, + )), + "[strict] barrier statements must have at least one argument", + ))); } else if let Some(num_gate_qubits) = num_gate_qubits { (0..num_gate_qubits).map(QubitId::new).collect::>() } else { diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 8727d78b810e..1d99ab34e679 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1716,6 +1716,10 @@ def qasm( elif operation.name == "reset": instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};" elif operation.name == "barrier": + if not instruction.qubits: + # Barriers with no operands are invalid in (strict) OQ2, and the statement + # would have no meaning anyway. + continue qargs = ",".join(bit_labels[q] for q in instruction.qubits) instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};" else: diff --git a/releasenotes/notes/qasm2-fix-zero-op-barrier-4af211b119d5b24d.yaml b/releasenotes/notes/qasm2-fix-zero-op-barrier-4af211b119d5b24d.yaml new file mode 100644 index 000000000000..9b86441fe814 --- /dev/null +++ b/releasenotes/notes/qasm2-fix-zero-op-barrier-4af211b119d5b24d.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + The OpenQASM 2 parser (:func:`.qasm2.load` and :func:`~.qasm2.loads`) running in ``strict`` mode + will now correctly emit an error if a ``barrier`` statement has no arguments. When running in + the (default) more permissive mode, an argument-less ``barrier`` statement will continue to + cause a barrier on all qubits currently in scope (the qubits a gate definition affects, or all + the qubits defined by a program, if the statement is in a gate body or in the global scope, + respectively). + - | + The OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`) will now no longer attempt + to output ``barrier`` statements that act on no qubits. Such a barrier statement has no effect + in Qiskit either, but is invalid OpenQASM 2. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 83e1d7999a47..0aa6ac6ececc 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -829,6 +829,23 @@ def test_sequencial_inner_gates_with_same_name(self): self.assertEqual(qc.qasm(), expected_output) + def test_empty_barrier(self): + """Test that a blank barrier statement in _Qiskit_ acts over all qubits, while an explicitly + no-op barrier (assuming Qiskit continues to allow this) is not output to OQ2 at all, since + the statement requires an argument in the spec.""" + qc = QuantumCircuit(QuantumRegister(2, "qr1"), QuantumRegister(3, "qr2")) + qc.barrier() # In Qiskit land, this affects _all_ qubits. + qc.barrier([]) # This explicitly affects _no_ qubits (so is totally meaningless). + + expected = """\ +OPENQASM 2.0; +include "qelib1.inc"; +qreg qr1[2]; +qreg qr2[3]; +barrier qr1[0],qr1[1],qr2[0],qr2[1],qr2[2]; +""" + self.assertEqual(qc.qasm(), expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/qasm2/test_parse_errors.py b/test/python/qasm2/test_parse_errors.py index 1cc90cdaef3c..1963fc64b3b6 100644 --- a/test/python/qasm2/test_parse_errors.py +++ b/test/python/qasm2/test_parse_errors.py @@ -865,3 +865,10 @@ def test_required_version_empty(self): qiskit.qasm2.QASM2ParseError, r"\[strict\] .*needed a version statement" ): qiskit.qasm2.loads("", strict=True) + + def test_barrier_requires_args(self): + program = "OPENQASM 2.0; qreg q[2]; barrier;" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"\[strict\] barrier statements must have at least one" + ): + qiskit.qasm2.loads(program, strict=True) From 6143c3f62c13b964b67c9ec62ce6d2b6d3d087fe Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 24 Jul 2023 22:05:33 +0200 Subject: [PATCH 06/19] bugfixing (#10452) --- qiskit/tools/jupyter/version_table.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/qiskit/tools/jupyter/version_table.py b/qiskit/tools/jupyter/version_table.py index 67681ee51674..959e155239c4 100644 --- a/qiskit/tools/jupyter/version_table.py +++ b/qiskit/tools/jupyter/version_table.py @@ -36,7 +36,14 @@ def qiskit_version_table(self, line="", cell=None): html += "" html += "" - packages = {} + packages = {"qiskit": None} + + packages["qiskit-terra"] = qiskit.__version__ + + qiskit_modules = {module.split(".")[0] for module in modules.keys() if "qiskit" in module} + + for qiskit_module in qiskit_modules: + packages[qiskit_module] = getattr(modules[qiskit_module], "__version__", None) from importlib.metadata import metadata, PackageNotFoundError @@ -45,14 +52,9 @@ def qiskit_version_table(self, line="", cell=None): except PackageNotFoundError: packages["qiskit"] = None - packages["qiskit-terra"] = qiskit.__version__ - - qiskit_modules = {module.split(".")[0] for module in modules.keys() if "qiskit" in module} - for qiskit_module in qiskit_modules: - packages[qiskit_module] = getattr(modules[qiskit_module], "__version__", None) - for name, version in packages.items(): - html += f"" + if name == "qiskit" or version: + html += f"" html += "" From 42a0ee84df3e15834b174bdb0295142b987b261f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 24 Jul 2023 17:18:58 -0400 Subject: [PATCH 07/19] Fix final_layout when VF2PostLayout finds a better layout (#10466) * Fix final_layout when VF2PostLayout finds a better layout This commit fixes a bug in the preset pass managers when VF2PostLayout is run and finds a better layout to use. In these cases the ApplyLayout was updating the layout but we never updated the final layout to reflect these changes. This would result in an incorrect final layout because the input positions of the qubits were incorrect after re-applying the layout. This commit fixes this by adding code to ApplyLayout to also update final_layout, if one is set, to reflect the new initial layout found by VF2PostLayout. Fixes #10457 * Remove stray debug print * Use a list instead of a dict * Add assertion on vf2postlayout being used in compiler.transpile tests * Actually assert a post layout is set --- .../transpiler/passes/layout/apply_layout.py | 9 ++++ ...yout-in-apply-layout-dfbdbde593cf7929.yaml | 15 ++++++ test/python/compiler/test_transpiler.py | 42 ++++++++++++++- test/python/transpiler/test_apply_layout.py | 53 ++++++++++++++++++- 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-final-layout-in-apply-layout-dfbdbde593cf7929.yaml diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 2f9305b23244..a69d90f22163 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -83,9 +83,11 @@ def run(self, dag): full_layout = Layout() old_phys_to_virtual = layout.get_physical_bits() new_virtual_to_physical = post_layout.get_virtual_bits() + phys_map = list(range(len(new_dag.qubits))) for new_virt, new_phys in new_virtual_to_physical.items(): old_phys = dag.find_bit(new_virt).index old_virt = old_phys_to_virtual[old_phys] + phys_map[old_phys] = new_phys full_layout.add(old_virt, new_phys) for reg in layout.get_registers(): full_layout.add_register(reg) @@ -94,6 +96,13 @@ def run(self, dag): qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs] new_dag.apply_operation_back(node.op, qargs, node.cargs) self.property_set["layout"] = full_layout + if (final_layout := self.property_set["final_layout"]) is not None: + final_layout_mapping = { + new_dag.qubits[phys_map[dag.find_bit(old_virt).index]]: phys_map[old_phys] + for old_virt, old_phys in final_layout.get_virtual_bits().items() + } + out_layout = Layout(final_layout_mapping) + self.property_set["final_layout"] = out_layout new_dag._global_phase = dag._global_phase return new_dag diff --git a/releasenotes/notes/fix-final-layout-in-apply-layout-dfbdbde593cf7929.yaml b/releasenotes/notes/fix-final-layout-in-apply-layout-dfbdbde593cf7929.yaml new file mode 100644 index 000000000000..f4297f55d8a7 --- /dev/null +++ b/releasenotes/notes/fix-final-layout-in-apply-layout-dfbdbde593cf7929.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + Fixed an issue with the :func:`~.transpile` function and all the preset + pass managers generated via :func:`~.generate_preset_pass_manager` where + the output :class:`~.QuantumCircuit` object's :attr:`~.QuantumCircuit.layout` + attribute would have an invalid :attr:`.TranspileLayout.final_layout` + attribute. This would occur in scenarios when the :class:`~.VF2PostLayout` + pass would run and find an alternative initial layout that has lower + reported error rates. When altering the initial layout the + :attr:`~.TranspileLayout.final_layout` attribute was never updated to + reflect this change. This has been corrected so that the ``final_layout`` + is always correctly reflecting the output permutation caused by the routing + stage. + Fixed `#10457 `__ diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 92187ec74999..614dad48722d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -67,6 +67,7 @@ FakeNairobiV2, FakeRueschlikon, FakeSherbrooke, + FakeVigo, ) from qiskit.providers.options import Options from qiskit.pulse import InstructionScheduleMap @@ -75,7 +76,7 @@ from qiskit.tools import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection +from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager from qiskit.transpiler.target import InstructionProperties, Target @@ -1857,6 +1858,45 @@ def test_transpile_target_no_measurement_error(self, opt_level): res = transpile(qc, target=target, optimization_level=opt_level) self.assertEqual(qc, res) + def test_transpile_final_layout_updated_with_post_layout(self): + """Test that the final layout is correctly set when vf2postlayout runs. + + Reproduce from #10457 + """ + + def _get_index_layout(transpiled_circuit: QuantumCircuit, num_source_qubits: int): + """Return the index layout of a transpiled circuit""" + layout = transpiled_circuit.layout + if layout is None: + return list(range(num_source_qubits)) + + pos_to_virt = {v: k for k, v in layout.input_qubit_mapping.items()} + qubit_indices = [] + for index in range(num_source_qubits): + qubit_idx = layout.initial_layout[pos_to_virt[index]] + if layout.final_layout is not None: + qubit_idx = layout.final_layout[transpiled_circuit.qubits[qubit_idx]] + qubit_indices.append(qubit_idx) + return qubit_indices + + vf2_post_layout_called = False + + def callback(**kwargs): + nonlocal vf2_post_layout_called + if isinstance(kwargs["pass_"], VF2PostLayout): + vf2_post_layout_called = True + self.assertIsNotNone(kwargs["property_set"]["post_layout"]) + + backend = FakeVigo() + qubits = 3 + qc = QuantumCircuit(qubits) + for i in range(5): + qc.cx(i % qubits, int(i + qubits / 2) % qubits) + + tqc = transpile(qc, backend=backend, seed_transpiler=4242, callback=callback) + self.assertTrue(vf2_post_layout_called) + self.assertEqual([3, 2, 1], _get_index_layout(tqc, qubits)) + class StreamHandlerRaiseException(StreamHandler): """Handler class that will raise an exception on formatting errors.""" diff --git a/test/python/transpiler/test_apply_layout.py b/test/python/transpiler/test_apply_layout.py index ae950912840d..d156db19dc18 100644 --- a/test/python/transpiler/test_apply_layout.py +++ b/test/python/transpiler/test_apply_layout.py @@ -18,8 +18,11 @@ from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.transpiler.layout import Layout -from qiskit.transpiler.passes import ApplyLayout +from qiskit.transpiler.passes import ApplyLayout, SetLayout from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.preset_passmanagers import common +from qiskit.providers.fake_provider import FakeVigoV2 +from qiskit.transpiler import PassManager class TestApplyLayout(QiskitTestCase): @@ -115,6 +118,54 @@ def test_circuit_with_swap_gate(self): self.assertEqual(circuit_to_dag(expected), after) + def test_final_layout_is_updated(self): + """Test that if vf2postlayout runs that we've updated the final layout.""" + qubits = 3 + qc = QuantumCircuit(qubits) + for i in range(5): + qc.cx(i % qubits, int(i + qubits / 2) % qubits) + initial_pm = PassManager([SetLayout([1, 3, 4])]) + cmap = FakeVigoV2().coupling_map + initial_pm += common.generate_embed_passmanager(cmap) + first_layout_circ = initial_pm.run(qc) + out_pass = ApplyLayout() + out_pass.property_set["layout"] = first_layout_circ.layout.initial_layout + out_pass.property_set[ + "original_qubit_indices" + ] = first_layout_circ.layout.input_qubit_mapping + out_pass.property_set["final_layout"] = Layout( + { + first_layout_circ.qubits[0]: 0, + first_layout_circ.qubits[1]: 3, + first_layout_circ.qubits[2]: 2, + first_layout_circ.qubits[3]: 4, + first_layout_circ.qubits[4]: 1, + } + ) + # Set a post layout like vf2postlayout would: + out_pass.property_set["post_layout"] = Layout( + { + first_layout_circ.qubits[0]: 0, + first_layout_circ.qubits[2]: 4, + first_layout_circ.qubits[1]: 2, + first_layout_circ.qubits[3]: 1, + first_layout_circ.qubits[4]: 3, + } + ) + out_pass(first_layout_circ) + self.assertEqual( + out_pass.property_set["final_layout"], + Layout( + { + first_layout_circ.qubits[0]: 0, + first_layout_circ.qubits[2]: 1, + first_layout_circ.qubits[4]: 4, + first_layout_circ.qubits[1]: 3, + first_layout_circ.qubits[3]: 2, + } + ), + ) + if __name__ == "__main__": unittest.main() From 07b06f9a0d27c2c09bea608a22df1ab8f169d686 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 25 Jul 2023 15:08:26 +0300 Subject: [PATCH 08/19] limiting matrix-based commutativity check (#10495) * limiting matrix-based commutativity check * adding tests with the order of gates fixed * lint tweaks * improved released notes --- qiskit/circuit/commutation_checker.py | 24 ++++++++++++++++--- ...-commutation-checker-66a1b9e90921621f.yaml | 14 +++++++++++ .../circuit/test_commutation_checker.py | 15 ++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-limits-in-commutation-checker-66a1b9e90921621f.yaml diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 5edceab1080f..ad9d4830997a 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -65,10 +65,20 @@ def _hashable_parameters(self, params): return ("fallback", str(params)) def commute( - self, op1: Operation, qargs1: List, cargs1: List, op2: Operation, qargs2: List, cargs2: List - ): + self, + op1: Operation, + qargs1: List, + cargs1: List, + op2: Operation, + qargs2: List, + cargs2: List, + max_num_qubits: int = 3, + ) -> bool: """ - Checks if two Operations commute. + Checks if two Operations commute. The return value of `True` means that the operations + truly commute, and the return value of `False` means that either the operations do not + commute or that the commutation check was skipped (for example, when the operations + have conditions or have too many qubits). Args: op1: first operation. @@ -77,10 +87,14 @@ def commute( op2: second operation. qargs2: second operation's qubits. cargs2: second operation's clbits. + max_num_qubits: the maximum number of qubits to consider, the check may be skipped if + the number of qubits for either operation exceeds this amount. Returns: bool: whether two operations commute. """ + # pylint: disable=too-many-return-statements + # We don't support commutation of conditional gates for now due to bugs in # CommutativeCancellation. See gh-8553. if ( @@ -105,6 +119,10 @@ def commute( if not (intersection_q or intersection_c): return True + # Skip the check if the number of qubits for either operation is too large + if len(qargs1) > max_num_qubits or len(qargs2) > max_num_qubits: + return False + # These lines are adapted from commutation_analysis, which is more restrictive than the # check from dag_dependency when considering nodes with "_directive". It would be nice to # think which optimizations from dag_dependency can indeed be used. diff --git a/releasenotes/notes/add-limits-in-commutation-checker-66a1b9e90921621f.yaml b/releasenotes/notes/add-limits-in-commutation-checker-66a1b9e90921621f.yaml new file mode 100644 index 000000000000..9b8bc6411083 --- /dev/null +++ b/releasenotes/notes/add-limits-in-commutation-checker-66a1b9e90921621f.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added a new option ``max_num_qubits`` to :meth:`qiskit.circuit.CommutationChecker.commute` + that specifies the maximum number of qubits to consider for the more expensive + matrix multiplication-based commutativity check. This avoids trying to + internally allocate arrays of size :math:`2^N \times 2^N`. Simpler versions of commutativity + check (for instance, two quantum operations commute when they are over disjoint sets of qubits) + continue to work without this limit. +fixes: + - | + The maximum number of qubits to consider for matrix multiplication-based commutativity check + in :class:`~.CommutationChecker` is now limited to 3 by default. + Fixed `#10488 `__ diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index cd1b08d0ceb9..bfb41e649a12 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -25,6 +25,7 @@ XGate, CXGate, CCXGate, + MCXGate, RZGate, Measure, Barrier, @@ -362,6 +363,20 @@ def test_c7x_gate(self): res = CommutationChecker().commute(XGate(), qargs[:1], [], XGate().control(7), qargs, []) self.assertFalse(res) + def test_wide_gates_over_nondisjoint_qubits(self): + """Test that checking wide gates does not lead to memory problems.""" + res = CommutationChecker().commute(MCXGate(29), list(range(30)), [], XGate(), [0], []) + self.assertFalse(res) + res = CommutationChecker().commute(XGate(), [0], [], MCXGate(29), list(range(30)), []) + self.assertFalse(res) + + def test_wide_gates_over_disjoint_qubits(self): + """Test that wide gates still commute when they are over disjoint sets of qubits.""" + res = CommutationChecker().commute(MCXGate(29), list(range(30)), [], XGate(), [30], []) + self.assertTrue(res) + res = CommutationChecker().commute(XGate(), [30], [], MCXGate(29), list(range(30)), []) + self.assertTrue(res) + if __name__ == "__main__": unittest.main() From f01b7ab8eb6c753a9a819af107d9daf37c3b7be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:55:16 +0200 Subject: [PATCH 09/19] Update algorithms deprecation message/reno (#10500) * Update reno * Update deprecation warning --- qiskit/algorithms/__init__.py | 7 +++++-- .../notes/0.25/deprecate-algorithms-7149dee2da586549.yaml | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 272d7be20bc5..3ee911b9c1c4 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -20,8 +20,11 @@ The :mod:`qiskit.algorithms` module has been migrated to an independent package: https://github.com/qiskit-community/qiskit-algorithms. The current import path is deprecated and will be removed no earlier - than 3 months after the release date. You can run ``pip install qiskit_algorithms`` - and import ``from qiskit_algorithms`` instead. + than 3 months after the release date. If your code uses primitives, you can run + ``pip install qiskit_algorithms`` and import ``from qiskit_algorithms`` instead. + If you use opflow/quantum instance-based algorithms, please update your code to + use primitives following: https://qisk.it/algo_migration before migrating to + the new package. It contains a collection of quantum algorithms, for use with quantum computers, to carry out research and investigate how to solve problems in different domains on diff --git a/releasenotes/notes/0.25/deprecate-algorithms-7149dee2da586549.yaml b/releasenotes/notes/0.25/deprecate-algorithms-7149dee2da586549.yaml index 646b2dfd62d7..4edaefe8bb7c 100644 --- a/releasenotes/notes/0.25/deprecate-algorithms-7149dee2da586549.yaml +++ b/releasenotes/notes/0.25/deprecate-algorithms-7149dee2da586549.yaml @@ -12,11 +12,13 @@ deprecations: of new features has moved to the new package. If you're relying on :mod:`qiskit.algorithms` you should update your requirements to also include ``qiskit-algorithms`` and update the imports - from ``qiskit.algorithms`` to ``qiskit_algorithms``. If you have not yet + from ``qiskit.algorithms`` to ``qiskit_algorithms``. Please note that this + new package does not include already deprecated algorithms code, including + ``opflow`` and ``QuantumInstance``-based algorithms. If you have not yet migrated from ``QuantumInstance``-based to primitives-based algorithms, you should follow the migration guidelines in https://qisk.it/algo_migration. - The decision to migrate the :mod:`~.algorithms` module to a - separate package was made to clarify the purpose Qiskit and + The decision to migrate the :mod:`~.algorithms` module to a + separate package was made to clarify the purpose Qiskit and make a distinction between the tools and libraries built on top of it. From eda570c58a21e04cf71576f40e7c3c91ee020cb4 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 26 Jul 2023 18:55:35 +0200 Subject: [PATCH 10/19] Consistent typehints for ``NLocal`` family (#10479) * consistent typehints * lint * attempt to fix docs 1/? * fix accidental default change * fix lint * fix , instead of | --- .../library/evolved_operator_ansatz.py | 3 +- .../circuit/library/n_local/efficient_su2.py | 38 ++++++------- .../library/n_local/excitation_preserving.py | 16 +++--- qiskit/circuit/library/n_local/n_local.py | 53 +++++++------------ qiskit/circuit/library/n_local/qaoa_ansatz.py | 3 +- .../library/n_local/real_amplitudes.py | 18 ++++--- qiskit/circuit/library/n_local/two_local.py | 37 ++++++++----- 7 files changed, 82 insertions(+), 86 deletions(-) diff --git a/qiskit/circuit/library/evolved_operator_ansatz.py b/qiskit/circuit/library/evolved_operator_ansatz.py index 5bde9c505698..5dd1914543b8 100644 --- a/qiskit/circuit/library/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/evolved_operator_ansatz.py @@ -14,7 +14,6 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Optional import numpy as np @@ -41,7 +40,7 @@ def __init__( name: str = "EvolvedOps", parameter_prefix: str | Sequence[str] = "t", initial_state: QuantumCircuit | None = None, - flatten: Optional[bool] = None, + flatten: bool | None = None, ): """ Args: diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index be7b346af862..50ac4626048f 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -12,13 +12,19 @@ """The EfficientSU2 2-local circuit.""" -from typing import Union, Optional, List, Tuple, Callable, Any +from __future__ import annotations +import typing +from collections.abc import Callable + from numpy import pi -from qiskit.circuit import QuantumCircuit, Instruction +from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import RYGate, RZGate, CXGate from .two_local import TwoLocal +if typing.TYPE_CHECKING: + import qiskit # pylint: disable=cyclic-import + class EfficientSU2(TwoLocal): r"""The hardware efficient SU(2) 2-local circuit. @@ -76,28 +82,24 @@ class EfficientSU2(TwoLocal): def __init__( self, - num_qubits: Optional[int] = None, - su2_gates: Optional[ - Union[ - str, - type, - Instruction, - QuantumCircuit, - List[Union[str, type, Instruction, QuantumCircuit]], - ] - ] = None, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "reverse_linear", + num_qubits: int | None = None, + su2_gates: str + | type + | qiskit.circuit.Instruction + | QuantumCircuit + | list[str | type | qiskit.circuit.Instruction | QuantumCircuit] + | None = None, + entanglement: str | list[list[int]] | Callable[[int], list[int]] = "reverse_linear", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, parameter_prefix: str = "θ", insert_barriers: bool = False, - initial_state: Optional[Any] = None, + initial_state: QuantumCircuit | None = None, name: str = "EfficientSU2", - flatten: Optional[bool] = None, + flatten: bool | None = None, ) -> None: - """Create a new EfficientSU2 2-local circuit. - + """ Args: num_qubits: The number of qubits of the EfficientSU2 circuit. reps: Specifies how often the structure of a rotation layer followed by an entanglement @@ -151,7 +153,7 @@ def __init__( ) @property - def parameter_bounds(self) -> List[Tuple[float, float]]: + def parameter_bounds(self) -> list[tuple[float, float]]: """Return the parameter bounds. Returns: diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index 3fc2b28bfb2a..7b01f825ecdb 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -12,7 +12,8 @@ """The ExcitationPreserving 2-local circuit.""" -from typing import Union, Optional, List, Tuple, Callable, Any +from __future__ import annotations +from collections.abc import Callable from numpy import pi from qiskit.circuit import QuantumCircuit, Parameter @@ -90,20 +91,19 @@ class ExcitationPreserving(TwoLocal): def __init__( self, - num_qubits: Optional[int] = None, + num_qubits: int | None = None, mode: str = "iswap", - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + entanglement: str | list[list[int]] | Callable[[int], list[int]] = "full", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, parameter_prefix: str = "θ", insert_barriers: bool = False, - initial_state: Optional[Any] = None, + initial_state: QuantumCircuit | None = None, name: str = "ExcitationPreserving", - flatten: Optional[bool] = None, + flatten: bool | None = None, ) -> None: - """Create a new ExcitationPreserving 2-local circuit. - + """ Args: num_qubits: The number of qubits of the ExcitationPreserving circuit. mode: Choose the entangler mode, can be `'iswap'` or `'fsim'`. @@ -167,7 +167,7 @@ def __init__( ) @property - def parameter_bounds(self) -> List[Tuple[float, float]]: + def parameter_bounds(self) -> list[tuple[float, float]]: """Return the parameter bounds. Returns: diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index d3d601a406f7..aca3ea65bb3b 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -13,9 +13,9 @@ """The n-local circuit class.""" from __future__ import annotations - import typing -from typing import Union, Optional, Any, Sequence, Callable, Mapping +from collections.abc import Callable, Mapping, Sequence + from itertools import combinations import numpy @@ -90,10 +90,9 @@ def __init__( skip_unentangled_qubits: bool = False, initial_state: QuantumCircuit | None = None, name: str | None = "nlocal", - flatten: Optional[bool] = None, + flatten: bool | None = None, ) -> None: - """Create a new n-local circuit. - + """ Args: num_qubits: The number of qubits of the circuit. rotation_blocks: The blocks used in the rotation layers. If multiple are passed, @@ -123,9 +122,6 @@ def __init__( to set this flag to ``True`` to avoid a large performance overhead for parameter binding. - Examples: - TODO - Raises: ValueError: If ``reps`` parameter is less than or equal to 0. TypeError: If ``reps`` parameter is not an int value. @@ -207,7 +203,7 @@ def flatten(self, flatten: bool) -> None: self._invalidate() self._flatten = flatten - def _convert_to_block(self, layer: Any) -> QuantumCircuit: + def _convert_to_block(self, layer: typing.Any) -> QuantumCircuit: """Try to convert ``layer`` to a QuantumCircuit. Args: @@ -289,17 +285,9 @@ def entanglement_blocks( @property def entanglement( self, - ) -> Union[ - str, - list[str], - list[list[str]], - list[int], - list[list[int]], - list[list[list[int]]], - list[list[list[list[int]]]], - Callable[[int], str], - Callable[[int], list[list[int]]], - ]: + ) -> str | list[str] | list[list[str]] | list[int] | list[list[int]] | list[ + list[list[int]] + ] | list[list[list[list[int]]]] | Callable[[int], str] | Callable[[int], list[list[int]]]: """Get the entanglement strategy. Returns: @@ -311,19 +299,16 @@ def entanglement( @entanglement.setter def entanglement( self, - entanglement: Optional[ - Union[ - str, - list[str], - list[list[str]], - list[int], - list[list[int]], - list[list[list[int]]], - list[list[list[list[int]]]], - Callable[[int], str], - Callable[[int], list[list[int]]], - ] - ], + entanglement: str + | list[str] + | list[list[str]] + | list[int] + | list[list[int]] + | list[list[list[int]]] + | list[list[list[list[int]]]] + | Callable[[int], str] + | Callable[[int], list[list[int]]] + | None, ) -> None: """Set the entanglement strategy. @@ -730,7 +715,7 @@ def parameter_bounds(self, bounds: list[tuple[float, float]]) -> None: def add_layer( self, - other: Union["NLocal", qiskit.circuit.Instruction, QuantumCircuit], + other: QuantumCircuit | qiskit.circuit.Instruction, entanglement: list[int] | str | list[list[int]] | None = None, front: bool = False, ) -> "NLocal": diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index 8161a1d9c8b4..b05507ea5068 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -14,7 +14,6 @@ # pylint: disable=cyclic-import from __future__ import annotations -from typing import Optional import numpy as np @@ -41,7 +40,7 @@ def __init__( initial_state: QuantumCircuit | None = None, mixer_operator=None, name: str = "QAOA", - flatten: Optional[bool] = None, + flatten: bool | None = None, ): r""" Args: diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index 5bc1a87b0c43..8fd8306f222e 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -12,9 +12,12 @@ """The real-amplitudes 2-local circuit.""" -from typing import Union, Optional, List, Tuple, Callable, Any +from __future__ import annotations +from collections.abc import Callable + import numpy as np +from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import RYGate, CXGate from .two_local import TwoLocal @@ -115,19 +118,18 @@ class RealAmplitudes(TwoLocal): def __init__( self, - num_qubits: Optional[int] = None, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "reverse_linear", + num_qubits: int | None = None, + entanglement: str | list[list[int]] | Callable[[int], list[int]] = "reverse_linear", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, parameter_prefix: str = "θ", insert_barriers: bool = False, - initial_state: Optional[Any] = None, + initial_state: QuantumCircuit | None = None, name: str = "RealAmplitudes", - flatten: Optional[bool] = None, + flatten: bool | None = None, ) -> None: - """Create a new RealAmplitudes 2-local circuit. - + """ Args: num_qubits: The number of qubits of the RealAmplitudes circuit. reps: Specifies how often the structure of a rotation layer followed by an entanglement @@ -178,7 +180,7 @@ def __init__( ) @property - def parameter_bounds(self) -> List[Tuple[float, float]]: + def parameter_bounds(self) -> list[tuple[float, float]]: """Return the parameter bounds. Returns: diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index 27a0c67ef6d7..e4e323d65052 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -13,7 +13,8 @@ """The two-local gate circuit.""" from __future__ import annotations -from typing import Union, Optional, List, Callable, Any, Sequence +import typing +from collections.abc import Callable, Sequence from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit import Gate, Instruction, Parameter @@ -46,6 +47,9 @@ CHGate, ) +if typing.TYPE_CHECKING: + import qiskit # pylint: disable=cyclic-import + class TwoLocal(NLocal): r"""The two-local circuit. @@ -158,25 +162,30 @@ class TwoLocal(NLocal): def __init__( self, - num_qubits: Optional[int] = None, - rotation_blocks: Optional[ - Union[str, List[str], type, List[type], QuantumCircuit, List[QuantumCircuit]] - ] = None, - entanglement_blocks: Optional[ - Union[str, List[str], type, List[type], QuantumCircuit, List[QuantumCircuit]] - ] = None, - entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + num_qubits: int | None = None, + rotation_blocks: str + | type + | qiskit.circuit.Instruction + | QuantumCircuit + | list[str | type | qiskit.circuit.Instruction | QuantumCircuit] + | None = None, + entanglement_blocks: str + | type + | qiskit.circuit.Instruction + | QuantumCircuit + | list[str | type | qiskit.circuit.Instruction | QuantumCircuit] + | None = None, + entanglement: str | list[list[int]] | Callable[[int], list[int]] = "full", reps: int = 3, skip_unentangled_qubits: bool = False, skip_final_rotation_layer: bool = False, parameter_prefix: str = "θ", insert_barriers: bool = False, - initial_state: Optional[Any] = None, + initial_state: QuantumCircuit | None = None, name: str = "TwoLocal", - flatten: Optional[bool] = None, + flatten: bool | None = None, ) -> None: - """Construct a new two-local circuit. - + """ Args: num_qubits: The number of qubits of the two-local circuit. rotation_blocks: The gates used in the rotation layer. Can be specified via the name of @@ -233,7 +242,7 @@ def __init__( flatten=flatten, ) - def _convert_to_block(self, layer: Union[str, type, Gate, QuantumCircuit]) -> QuantumCircuit: + def _convert_to_block(self, layer: str | type | Gate | QuantumCircuit) -> QuantumCircuit: """For a layer provided as str (e.g. ``'ry'``) or type (e.g. :class:`.RYGate`) this function returns the according layer type along with the number of parameters (e.g. ``(RYGate, 1)``). From 7af335e5cd13a102b0074e6ea6a387eae963538c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 26 Jul 2023 19:53:29 +0100 Subject: [PATCH 11/19] Add full `Expr` support to `StochasticSwap` (#10506) This was mostly already there (and why I had missed problems when testing before), the missing piece was just for `switch` statements. This commit also adds some final tests of the pass. --- .../passes/routing/stochastic_swap.py | 12 +- .../python/transpiler/test_stochastic_swap.py | 139 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 067d955f91b7..5bc7c97f31f7 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.converters import dag_to_circuit, circuit_to_dag +from qiskit.circuit.classical import expr, types from qiskit.circuit.quantumregister import QuantumRegister from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -474,7 +475,16 @@ def _controlflow_exhaustive_acyclic(operation: ControlFlowOp): return len(operation.blocks) == 2 if isinstance(operation, SwitchCaseOp): cases = operation.cases() - max_matches = 2 if isinstance(operation.target, Clbit) else 1 << len(operation.target) + if isinstance(operation.target, expr.Expr): + type_ = operation.target.type + if type_.kind is types.Bool: + max_matches = 2 + elif type_.kind is types.Uint: + max_matches = 1 << type_.width + else: + raise RuntimeError(f"unhandled target type: '{type_}'") + else: + max_matches = 2 if isinstance(operation.target, Clbit) else 1 << len(operation.target) return CASE_DEFAULT in cases or len(cases) == max_matches return False diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 152889f9512a..6ef3c01f2924 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -29,6 +29,7 @@ from qiskit.providers.fake_provider import FakeMumbai, FakeMumbaiV2 from qiskit.compiler.transpiler import transpile from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT +from qiskit.circuit.classical import expr @ddt @@ -882,6 +883,44 @@ def test_pre_intra_post_if_else(self): expected.measure(qreg, creg[[2, 4, 0, 3, 1]]) self.assertEqual(dag_to_circuit(cdag), expected) + def test_if_expr(self): + """Test simple if conditional with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + body = QuantumCircuit(4) + body.cx(0, 1) + body.cx(0, 2) + body.cx(0, 3) + qc = QuantumCircuit(4, 2) + qc.if_test(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + + def test_if_else_expr(self): + """Test simple if/else conditional with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + true = QuantumCircuit(4) + true.cx(0, 1) + true.cx(0, 2) + true.cx(0, 3) + false = QuantumCircuit(4) + false.cx(3, 0) + false.cx(3, 1) + false.cx(3, 2) + qc = QuantumCircuit(4, 2) + qc.if_else(expr.logic_and(qc.clbits[0], qc.clbits[1]), true, false, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_no_layout_change(self): """test controlflow with no layout change needed""" num_qubits = 5 @@ -996,6 +1035,23 @@ def test_while_loop(self): expected.measure(qreg, creg) self.assertEqual(dag_to_circuit(cdag), expected) + def test_while_loop_expr(self): + """Test simple while loop with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + body = QuantumCircuit(4) + body.cx(0, 1) + body.cx(0, 2) + body.cx(0, 3) + qc = QuantumCircuit(4, 2) + qc.while_loop(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = StochasticSwap(coupling, seed=58).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_switch_single_case(self): """Test routing of 'switch' with just a single case.""" qreg = QuantumRegister(5, "q") @@ -1110,6 +1166,89 @@ def test_switch_exhaustive(self, labels): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_switch_nonexhaustive_expr(self): + """Test routing of 'switch' with an `Expr` target and several but nonexhaustive cases.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(3, "c") + + qc = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.cx(3, 1) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.cx(4, 2) + qc.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + case0.swap(0, 1) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.swap(1, 2) + case1.cx(3, 2) + case1.swap(1, 2) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.swap(3, 4) + case2.cx(3, 2) + case2.swap(3, 4) + expected.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + @data((0, 1, 2, 3), (CASE_DEFAULT,)) + def test_switch_exhaustive_expr(self, labels): + """Test routing of 'switch' with exhaustive cases on an `Expr` target; we should not require + restoring the layout afterwards.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(2, "c") + + qc = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + qc.switch(expr.bit_or(creg, 3), [(labels, case0)], qreg[[0, 1, 2]], creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + expected.switch(expr.bit_or(creg, 3), [(labels, case0)], qreg[[0, 1, 2]], creg) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_nested_inner_cnot(self): """test swap in nested if else controlflow construct; swap in inner""" seed = 1 From 025320f7044ca3df13fb10108924144ede63a203 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 27 Jul 2023 05:31:19 -0600 Subject: [PATCH 12/19] Document functions on module pages (#10471) * Document functions on module pages * Fix invalid functions * Revert visualization because each function is so big * Fix more issues * Use currentmodule * Show modules & fix recursion * Add some missing autofunctions --------- Co-authored-by: Jake Lishman --- docs/conf.py | 9 +- qiskit/algorithms/__init__.py | 26 ++- qiskit/assembler/__init__.py | 15 +- qiskit/circuit/__init__.py | 7 +- qiskit/circuit/library/__init__.py | 153 +++++++++--------- qiskit/compiler/__init__.py | 11 +- qiskit/converters/__init__.py | 21 ++- qiskit/opflow/__init__.py | 14 +- qiskit/providers/__init__.py | 11 +- qiskit/pulse/__init__.py | 2 +- qiskit/pulse/builder.py | 102 +++++------- qiskit/pulse/library/__init__.py | 29 ++-- qiskit/pulse/transforms/__init__.py | 31 ++-- qiskit/qasm/__init__.py | 5 +- qiskit/qpy/__init__.py | 7 +- qiskit/quantum_info/__init__.py | 73 ++++----- qiskit/result/__init__.py | 12 +- qiskit/scheduler/__init__.py | 8 +- qiskit/scheduler/methods/__init__.py | 6 +- qiskit/synthesis/__init__.py | 55 +++---- qiskit/tools/__init__.py | 14 +- qiskit/tools/events/__init__.py | 10 +- .../preset_passmanagers/__init__.py | 34 ++-- .../transpiler/preset_passmanagers/plugin.py | 5 +- qiskit/utils/__init__.py | 41 ++--- qiskit/visualization/__init__.py | 5 +- 26 files changed, 289 insertions(+), 417 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 172da360265d..9dfbb3f73fcf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,8 +57,13 @@ pygments_style = "colorful" -# Whether module names are included in crossrefs of functions, classes, etc. -add_module_names = False +# This adds the module name to e.g. function API docs. We use the default of True because our +# module pages sometimes have functions from submodules on the page, and we want to make clear +# that you must include the submodule to import it. We should strongly consider reorganizing our +# code to avoid this, i.e. re-exporting the submodule members from the top-level module. Once fixed +# and verified by only having a single `.. currentmodule::` in the file, we can turn this back to +# False. +add_module_names = True # A list of prefixes that are ignored for sorting the Python module index # (e.g., if this is set to ['foo.'], then foo.bar is shown under B, not F). diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 3ee911b9c1c4..18767f1fdd01 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -280,23 +280,7 @@ Exceptions ---------- -.. autosummary:: - :toctree: ../stubs/ - - AlgorithmError - - -Utility methods ---------------- - -Utility methods used by algorithms. - -.. autosummary:: - :toctree: ../stubs/ - - eval_observables - estimate_observables - +.. autoexception:: AlgorithmError Utility classes --------------- @@ -308,6 +292,14 @@ AlgorithmJob +Utility functions +----------------- + +Utility functions used by algorithms. + +.. autofunction:: eval_observables +.. autofunction:: estimate_observables + """ import warnings diff --git a/qiskit/assembler/__init__.py b/qiskit/assembler/__init__.py index ee4dfa7e2196..a356501a263c 100644 --- a/qiskit/assembler/__init__.py +++ b/qiskit/assembler/__init__.py @@ -20,26 +20,17 @@ Circuit Assembler ================= -.. autosummary:: - :toctree: ../stubs/ - - assemble_circuits +.. autofunction:: assemble_circuits Schedule Assembler ================== -.. autosummary:: - :toctree: ../stubs/ - - assemble_schedules +.. autofunction:: assemble_schedules Disassembler ============ -.. autosummary:: - :toctree: ../stubs/ - - disassemble +.. autofunction:: disassemble RunConfig ========= diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 7c0548db797a..079ead121c85 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -345,10 +345,9 @@ Random Circuits --------------- -.. autosummary:: - :toctree: ../stubs/ - - random.random_circuit +.. currentmodule:: qiskit.circuit.random +.. autofunction:: random_circuit +.. currentmodule:: qiskit.circuit """ from .quantumcircuit import QuantumCircuit from .classicalregister import ClassicalRegister, Clbit diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 4d530fc79ca5..2e3637924dab 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -394,98 +394,91 @@ Techniques for the synthesis of reversible Toffoli networks, 2007 http://dx.doi.org/10.1145/1278349.1278355 -.. autosummary:: - :toctree: ../stubs/ - - templates.nct.template_nct_2a_1 - templates.nct.template_nct_2a_2 - templates.nct.template_nct_2a_3 - templates.nct.template_nct_4a_1 - templates.nct.template_nct_4a_2 - templates.nct.template_nct_4a_3 - templates.nct.template_nct_4b_1 - templates.nct.template_nct_4b_2 - templates.nct.template_nct_5a_1 - templates.nct.template_nct_5a_2 - templates.nct.template_nct_5a_3 - templates.nct.template_nct_5a_4 - templates.nct.template_nct_6a_1 - templates.nct.template_nct_6a_2 - templates.nct.template_nct_6a_3 - templates.nct.template_nct_6a_4 - templates.nct.template_nct_6b_1 - templates.nct.template_nct_6b_2 - templates.nct.template_nct_6c_1 - templates.nct.template_nct_7a_1 - templates.nct.template_nct_7b_1 - templates.nct.template_nct_7c_1 - templates.nct.template_nct_7d_1 - templates.nct.template_nct_7e_1 - templates.nct.template_nct_9a_1 - templates.nct.template_nct_9c_1 - templates.nct.template_nct_9c_2 - templates.nct.template_nct_9c_3 - templates.nct.template_nct_9c_4 - templates.nct.template_nct_9c_5 - templates.nct.template_nct_9c_6 - templates.nct.template_nct_9c_7 - templates.nct.template_nct_9c_8 - templates.nct.template_nct_9c_9 - templates.nct.template_nct_9c_10 - templates.nct.template_nct_9c_11 - templates.nct.template_nct_9c_12 - templates.nct.template_nct_9d_1 - templates.nct.template_nct_9d_2 - templates.nct.template_nct_9d_3 - templates.nct.template_nct_9d_4 - templates.nct.template_nct_9d_5 - templates.nct.template_nct_9d_6 - templates.nct.template_nct_9d_7 - templates.nct.template_nct_9d_8 - templates.nct.template_nct_9d_9 - templates.nct.template_nct_9d_10 +.. currentmodule:: qiskit.circuit.library.templates.nct +.. autofunction:: template_nct_2a_1 +.. autofunction:: template_nct_2a_2 +.. autofunction:: template_nct_2a_3 +.. autofunction:: template_nct_4a_1 +.. autofunction:: template_nct_4a_2 +.. autofunction:: template_nct_4a_3 +.. autofunction:: template_nct_4b_1 +.. autofunction:: template_nct_4b_2 +.. autofunction:: template_nct_5a_1 +.. autofunction:: template_nct_5a_2 +.. autofunction:: template_nct_5a_3 +.. autofunction:: template_nct_5a_4 +.. autofunction:: template_nct_6a_1 +.. autofunction:: template_nct_6a_2 +.. autofunction:: template_nct_6a_3 +.. autofunction:: template_nct_6a_4 +.. autofunction:: template_nct_6b_1 +.. autofunction:: template_nct_6b_2 +.. autofunction:: template_nct_6c_1 +.. autofunction:: template_nct_7a_1 +.. autofunction:: template_nct_7b_1 +.. autofunction:: template_nct_7c_1 +.. autofunction:: template_nct_7d_1 +.. autofunction:: template_nct_7e_1 +.. autofunction:: template_nct_9a_1 +.. autofunction:: template_nct_9c_1 +.. autofunction:: template_nct_9c_2 +.. autofunction:: template_nct_9c_3 +.. autofunction:: template_nct_9c_4 +.. autofunction:: template_nct_9c_5 +.. autofunction:: template_nct_9c_6 +.. autofunction:: template_nct_9c_7 +.. autofunction:: template_nct_9c_8 +.. autofunction:: template_nct_9c_9 +.. autofunction:: template_nct_9c_10 +.. autofunction:: template_nct_9c_11 +.. autofunction:: template_nct_9c_12 +.. autofunction:: template_nct_9d_1 +.. autofunction:: template_nct_9d_2 +.. autofunction:: template_nct_9d_3 +.. autofunction:: template_nct_9d_4 +.. autofunction:: template_nct_9d_5 +.. autofunction:: template_nct_9d_6 +.. autofunction:: template_nct_9d_7 +.. autofunction:: template_nct_9d_8 +.. autofunction:: template_nct_9d_9 +.. autofunction:: template_nct_9d_10 +.. currentmodule:: qiskit.circuit.library Clifford template circuits -------------------------- Template circuits over Clifford gates. -.. autosummary:: - :toctree: ../stubs/ - - clifford_2_1 - clifford_2_2 - clifford_2_3 - clifford_2_4 - clifford_3_1 - clifford_4_1 - clifford_4_2 - clifford_4_3 - clifford_4_4 - clifford_5_1 - clifford_6_1 - clifford_6_2 - clifford_6_3 - clifford_6_4 - clifford_6_5 - clifford_8_1 - clifford_8_2 - clifford_8_3 +.. autofunction:: clifford_2_1 +.. autofunction:: clifford_2_2 +.. autofunction:: clifford_2_3 +.. autofunction:: clifford_2_4 +.. autofunction:: clifford_3_1 +.. autofunction:: clifford_4_1 +.. autofunction:: clifford_4_2 +.. autofunction:: clifford_4_3 +.. autofunction:: clifford_4_4 +.. autofunction:: clifford_5_1 +.. autofunction:: clifford_6_1 +.. autofunction:: clifford_6_2 +.. autofunction:: clifford_6_3 +.. autofunction:: clifford_6_4 +.. autofunction:: clifford_6_5 +.. autofunction:: clifford_8_1 +.. autofunction:: clifford_8_2 +.. autofunction:: clifford_8_3 RZXGate template circuits ------------------------- Template circuits with :class:`~qiskit.circuit.library.RZXGate`. -.. autosummary:: - :toctree: ../stubs/ - - rzx_yz - rzx_xz - rzx_cy - rzx_zz1 - rzx_zz2 - rzx_zz3 +.. autofunction:: rzx_yz +.. autofunction:: rzx_xz +.. autofunction:: rzx_cy +.. autofunction:: rzx_zz1 +.. autofunction:: rzx_zz2 +.. autofunction:: rzx_zz3 """ diff --git a/qiskit/compiler/__init__.py b/qiskit/compiler/__init__.py index fd6429260db4..4974fe68a4cd 100644 --- a/qiskit/compiler/__init__.py +++ b/qiskit/compiler/__init__.py @@ -20,13 +20,10 @@ Circuit and Pulse Compilation Functions ======================================= -.. autosummary:: - :toctree: ../stubs/ - - assemble - schedule - transpile - sequence +.. autofunction:: assemble +.. autofunction:: schedule +.. autofunction:: transpile +.. autofunction:: sequence """ diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index 2e7c0a9092f0..afac4cd2231f 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -17,18 +17,15 @@ .. currentmodule:: qiskit.converters -.. autosummary:: - :toctree: ../stubs/ - - circuit_to_dag - dag_to_circuit - circuit_to_instruction - circuit_to_gate - ast_to_dag - dagdependency_to_circuit - circuit_to_dagdependency - dag_to_dagdependency - dagdependency_to_dag +.. autofunction:: circuit_to_dag +.. autofunction:: dag_to_circuit +.. autofunction:: circuit_to_instruction +.. autofunction:: circuit_to_gate +.. autofunction:: ast_to_dag +.. autofunction:: dagdependency_to_circuit +.. autofunction:: circuit_to_dagdependency +.. autofunction:: dag_to_dagdependency +.. autofunction:: dagdependency_to_dag """ from .circuit_to_dag import circuit_to_dag diff --git a/qiskit/opflow/__init__.py b/qiskit/opflow/__init__.py index 9551dd3f318b..babed3da26b5 100644 --- a/qiskit/opflow/__init__.py +++ b/qiskit/opflow/__init__.py @@ -149,21 +149,15 @@ Utility functions ================= -.. autosummary:: - :toctree: ../stubs/ - - commutator - anti_commutator - double_commutator +.. autofunction:: commutator +.. autofunction:: anti_commutator +.. autofunction:: double_commutator Exceptions ========== -.. autosummary:: - :toctree: ../stubs/ - - OpflowError +.. autoexception:: OpflowError """ import warnings diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index e581c9721fe0..7b1ef8527049 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -125,13 +125,10 @@ Exceptions ---------- -.. autosummary:: - :toctree: ../stubs/ - - QiskitBackendNotFoundError - BackendPropertyError - JobError - JobTimeoutError +.. autoexception:: QiskitBackendNotFoundError +.. autoexception:: BackendPropertyError +.. autoexception:: JobError +.. autoexception:: JobTimeoutError ====================== Writing a New Provider diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 07ebcba99b7b..d8c42734cebf 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -54,7 +54,7 @@ Exceptions ========== -.. autoclass:: PulseError +.. autoexception:: PulseError """ # Builder imports. diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index b5410776df14..913384a56f3c 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -231,10 +231,7 @@ The above is just a small taste of what is possible with the builder. See the rest of the module documentation for more information on its capabilities. -.. autosummary:: - :toctree: ../stubs/ - - build +.. autofunction:: build Channels @@ -257,13 +254,10 @@ DriveChannel(0) -.. autosummary:: - :toctree: ../stubs/ - - acquire_channel - control_channels - drive_channel - measure_channel +.. autofunction:: acquire_channel +.. autofunction:: control_channels +.. autofunction:: drive_channel +.. autofunction:: measure_channel Instructions @@ -299,21 +293,17 @@ drive_sched.draw() - -.. autosummary:: - :toctree: ../stubs/ - - acquire - barrier - call - delay - play - reference - set_frequency - set_phase - shift_frequency - shift_phase - snapshot +.. autofunction:: acquire +.. autofunction:: barrier +.. autofunction:: call +.. autofunction:: delay +.. autofunction:: play +.. autofunction:: reference +.. autofunction:: set_frequency +.. autofunction:: set_phase +.. autofunction:: shift_frequency +.. autofunction:: shift_phase +.. autofunction:: snapshot Contexts @@ -340,18 +330,15 @@ pulse_prog.draw() -.. autosummary:: - :toctree: ../stubs/ - - align_equispaced - align_func - align_left - align_right - align_sequential - circuit_scheduler_settings - frequency_offset - phase_offset - transpiler_settings +.. autofunction:: align_equispaced +.. autofunction:: align_func +.. autofunction:: align_left +.. autofunction:: align_right +.. autofunction:: align_sequential +.. autofunction:: circuit_scheduler_settings +.. autofunction:: frequency_offset +.. autofunction:: phase_offset +.. autofunction:: transpiler_settings Macros @@ -374,12 +361,9 @@ MemorySlot(0) -.. autosummary:: - :toctree: ../stubs/ - - measure - measure_all - delay_qubits +.. autofunction:: measure +.. autofunction:: measure_all +.. autofunction:: delay_qubits Circuit Gates @@ -405,14 +389,11 @@ with pulse.build(backend) as u3_sched: pulse.u3(math.pi, 0, math.pi, 0) -.. autosummary:: - :toctree: ../stubs/ - - cx - u1 - u2 - u3 - x +.. autofunction:: cx +.. autofunction:: u1 +.. autofunction:: u2 +.. autofunction:: u3 +.. autofunction:: x Utilities @@ -446,16 +427,13 @@ There are 160 samples in 3.5555555555555554e-08 seconds There are 1e-06 seconds in 4500 samples. -.. autosummary:: - :toctree: ../stubs/ - - active_backend - active_transpiler_settings - active_circuit_scheduler_settings - num_qubits - qubit_channels - samples_to_seconds - seconds_to_samples +.. autofunction:: active_backend +.. autofunction:: active_transpiler_settings +.. autofunction:: active_circuit_scheduler_settings +.. autofunction:: num_qubits +.. autofunction:: qubit_channels +.. autofunction:: samples_to_seconds +.. autofunction:: seconds_to_samples """ import collections import contextvars diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 2562f96f24ef..d8809607e3d9 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -57,22 +57,19 @@ Waveform Pulse Representation ============================= -.. autosummary:: - :toctree: ../stubs/ - - constant - zero - square - sawtooth - triangle - cos - sin - gaussian - gaussian_deriv - sech - sech_deriv - gaussian_square - drag +.. autofunction:: constant +.. autofunction:: zero +.. autofunction:: square +.. autofunction:: sawtooth +.. autofunction:: triangle +.. autofunction:: cos +.. autofunction:: sin +.. autofunction:: gaussian +.. autofunction:: gaussian_deriv +.. autofunction:: sech +.. autofunction:: sech_deriv +.. autofunction:: gaussian_square +.. autofunction:: drag .. _symbolic_pulses: diff --git a/qiskit/pulse/transforms/__init__.py b/qiskit/pulse/transforms/__init__.py index fcc5557bf565..4fb4bf40e9ef 100644 --- a/qiskit/pulse/transforms/__init__.py +++ b/qiskit/pulse/transforms/__init__.py @@ -47,18 +47,15 @@ The canonicalization transforms convert schedules to a form amenable for execution on OpenPulse backends. -.. autosummary:: - :toctree: ../stubs/ - - add_implicit_acquires - align_measures - block_to_schedule - compress_pulses - flatten - inline_subroutines - pad - remove_directives - remove_trivial_barriers +.. autofunction:: add_implicit_acquires +.. autofunction:: align_measures +.. autofunction:: block_to_schedule +.. autofunction:: compress_pulses +.. autofunction:: flatten +.. autofunction:: inline_subroutines +.. autofunction:: pad +.. autofunction:: remove_directives +.. autofunction:: remove_trivial_barriers .. _pulse_dag: @@ -69,10 +66,7 @@ The DAG transforms create DAG representation of input program. This can be used for optimization of instructions and equality checks. -.. autosummary:: - :toctree: ../stubs/ - - block_to_dag +.. autofunction:: block_to_dag .. _pulse_transform_chain: @@ -82,10 +76,7 @@ A sequence of transformations to generate a target code. -.. autosummary:: - :toctree: ../stubs/ - - target_qobj_transform +.. autofunction:: target_qobj_transform """ diff --git a/qiskit/qasm/__init__.py b/qiskit/qasm/__init__.py index f442aaef845f..322d230343c6 100644 --- a/qiskit/qasm/__init__.py +++ b/qiskit/qasm/__init__.py @@ -20,10 +20,7 @@ QASM Routines ============= -.. autosummary:: - :toctree: ../stubs/ - - Qasm +.. autoclass:: Qasm Pygments diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 631b6865fea1..4482fb0753bd 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -71,11 +71,8 @@ API documentation ================= -.. autosummary:: - :toctree: ../stubs/ - - load - dump +.. autofunction:: load +.. autofunction:: dump QPY Compatibility ================= diff --git a/qiskit/quantum_info/__init__.py b/qiskit/quantum_info/__init__.py index c507e0d2bed4..6021955e8d54 100644 --- a/qiskit/quantum_info/__init__.py +++ b/qiskit/quantum_info/__init__.py @@ -62,59 +62,51 @@ Measures ======== -.. autosummary:: - :toctree: ../stubs/ - - average_gate_fidelity - process_fidelity - gate_error - diamond_norm - state_fidelity - purity - concurrence - entropy - entanglement_of_formation - mutual_information +.. autofunction:: average_gate_fidelity +.. autofunction:: process_fidelity +.. autofunction:: gate_error +.. autofunction:: diamond_norm +.. autofunction:: state_fidelity +.. autofunction:: purity +.. autofunction:: concurrence +.. autofunction:: entropy +.. autofunction:: entanglement_of_formation +.. autofunction:: mutual_information Utility Functions ================= -.. autosummary:: - :toctree: ../stubs/ - - partial_trace - schmidt_decomposition - shannon_entropy - commutator - anti_commutator - double_commutator +.. autofunction:: partial_trace +.. autofunction:: schmidt_decomposition +.. autofunction:: shannon_entropy +.. autofunction:: commutator +.. autofunction:: anti_commutator +.. autofunction:: double_commutator Random ====== -.. autosummary:: - :toctree: ../stubs/ - - random_statevector - random_density_matrix - random_unitary - random_hermitian - random_pauli - random_clifford - random_quantum_channel - random_cnotdihedral - random_pauli_table - random_pauli_list - random_stabilizer_table +.. autofunction:: random_statevector +.. autofunction:: random_density_matrix +.. autofunction:: random_unitary +.. autofunction:: random_hermitian +.. autofunction:: random_pauli +.. autofunction:: random_clifford +.. autofunction:: random_quantum_channel +.. autofunction:: random_cnotdihedral +.. autofunction:: random_pauli_table +.. autofunction:: random_pauli_list +.. autofunction:: random_stabilizer_table Analysis ========= +.. autofunction:: hellinger_distance +.. autofunction:: hellinger_fidelity + .. autosummary:: :toctree: ../stubs/ - hellinger_distance - hellinger_fidelity Z2Symmetries Synthesis @@ -125,10 +117,11 @@ OneQubitEulerDecomposer TwoQubitBasisDecomposer - two_qubit_cnot_decompose Quaternion - decompose_clifford XXDecomposer + +.. autofunction:: two_qubit_cnot_decompose +.. autofunction:: decompose_clifford """ from __future__ import annotations diff --git a/qiskit/result/__init__.py b/qiskit/result/__init__.py index 024df50506b1..08b43f704939 100644 --- a/qiskit/result/__init__.py +++ b/qiskit/result/__init__.py @@ -23,9 +23,10 @@ Result ResultError Counts - marginal_counts - marginal_distribution - marginal_memory + +.. autofunction:: marginal_counts +.. autofunction:: marginal_distribution +.. autofunction:: marginal_memory Distributions ============= @@ -39,10 +40,7 @@ Expectation values ================== -.. autosummary:: - :toctree: ../stubs/ - - sampled_expectation_value +.. autofunction:: sampled_expectation_value Mitigation ========== diff --git a/qiskit/scheduler/__init__.py b/qiskit/scheduler/__init__.py index b9f2ef97ada3..b85ee77d3764 100644 --- a/qiskit/scheduler/__init__.py +++ b/qiskit/scheduler/__init__.py @@ -19,11 +19,11 @@ A circuit scheduler compiles a circuit program to a pulse program. -.. autosummary:: - :toctree: ../stubs/ +.. autoclass:: ScheduleConfig - schedule_circuit - ScheduleConfig +.. currentmodule:: qiskit.scheduler.schedule_circuit +.. autofunction:: schedule_circuit +.. currentmodule:: qiskit.scheduler .. automodule:: qiskit.scheduler.methods """ diff --git a/qiskit/scheduler/methods/__init__.py b/qiskit/scheduler/methods/__init__.py index b41135d6a5c6..1fe4b301b7ab 100644 --- a/qiskit/scheduler/methods/__init__.py +++ b/qiskit/scheduler/methods/__init__.py @@ -15,10 +15,8 @@ Pulse scheduling methods. -.. autosummary:: - :toctree: ../stubs/ - - basic +.. autofunction:: as_soon_as_possible +.. autofunction:: as_late_as_possible """ from qiskit.scheduler.methods.basic import as_soon_as_possible, as_late_as_possible diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index ff7a3b025a8f..c312ae0dfebd 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -34,61 +34,45 @@ Linear Function Synthesis ========================= -.. autosummary:: - :toctree: ../stubs/ - synth_cnot_count_full_pmh - synth_cnot_depth_line_kms +.. autofunction:: synth_cnot_count_full_pmh +.. autofunction:: synth_cnot_depth_line_kms Linear-Phase Synthesis ====================== -.. autosummary:: - :toctree: ../stubs/ - synth_cz_depth_line_mr - synth_cx_cz_depth_line_my +.. autofunction:: synth_cz_depth_line_mr +.. autofunction:: synth_cx_cz_depth_line_my Permutation Synthesis ===================== -.. autosummary:: - :toctree: ../stubs/ - - synth_permutation_depth_lnn_kms - synth_permutation_basic - synth_permutation_acg +.. autofunction:: synth_permutation_depth_lnn_kms +.. autofunction:: synth_permutation_basic +.. autofunction:: synth_permutation_acg Clifford Synthesis ================== -.. autosummary:: - :toctree: ../stubs/ - - synth_clifford_full - synth_clifford_ag - synth_clifford_bm - synth_clifford_greedy - synth_clifford_layers - synth_clifford_depth_lnn +.. autofunction:: synth_clifford_full +.. autofunction:: synth_clifford_ag +.. autofunction:: synth_clifford_bm +.. autofunction:: synth_clifford_greedy +.. autofunction:: synth_clifford_layers +.. autofunction:: synth_clifford_depth_lnn CNOTDihedral Synthesis ====================== -.. autosummary:: - :toctree: ../stubs/ - - synth_cnotdihedral_full - synth_cnotdihedral_two_qubits - synth_cnotdihedral_general +.. autofunction:: synth_cnotdihedral_full +.. autofunction:: synth_cnotdihedral_two_qubits +.. autofunction:: synth_cnotdihedral_general Stabilizer State Synthesis ========================== -.. autosummary:: - :toctree: ../stubs/ - - synth_stabilizer_layers - synth_stabilizer_depth_lnn +.. autofunction:: synth_stabilizer_layers +.. autofunction:: synth_stabilizer_depth_lnn Discrete Basis Synthesis ======================== @@ -97,7 +81,8 @@ :toctree: ../stubs/ SolovayKitaevDecomposition - generate_basic_approximations + +.. autofunction:: generate_basic_approximations """ diff --git a/qiskit/tools/__init__.py b/qiskit/tools/__init__.py index 4f6f8e02879b..6bddd22ba889 100644 --- a/qiskit/tools/__init__.py +++ b/qiskit/tools/__init__.py @@ -24,22 +24,16 @@ Tasks can be executed in parallel using this function. It has a built-in event publisher to show the progress of the parallel tasks. -.. autosummary:: - :toctree: ../stubs/ - - parallel_map +.. autofunction:: parallel_map Monitoring ---------- A helper module to get IBM backend information and submitted job status. -.. autosummary:: - :toctree: ../stubs/ - - job_monitor - backend_monitor - backend_overview +.. autofunction:: job_monitor +.. autofunction:: backend_monitor +.. autofunction:: backend_overview .. automodule:: qiskit.tools.events diff --git a/qiskit/tools/events/__init__.py b/qiskit/tools/events/__init__.py index 9ada164d8d06..d15a537bb942 100644 --- a/qiskit/tools/events/__init__.py +++ b/qiskit/tools/events/__init__.py @@ -19,15 +19,7 @@ .. currentmodule:: qiskit.tools.events -TextProgressBar ---------------- - -A text based progress bar, which also enables Jupyter magics. - -.. autosummary:: - :toctree: ../stubs/ - - TextProgressBar +.. autoclass:: TextProgressBar """ from .progressbar import TextProgressBar diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index e38b1effd57c..4a0ddc10dc0f 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -34,31 +34,27 @@ Preset Pass Manager Generation ------------------------------ -.. autosummary:: - :toctree: ../stubs/ - - generate_preset_pass_manager - level_0_pass_manager - level_1_pass_manager - level_2_pass_manager - level_3_pass_manager +.. autofunction:: generate_preset_pass_manager +.. autofunction:: level_0_pass_manager +.. autofunction:: level_1_pass_manager +.. autofunction:: level_2_pass_manager +.. autofunction:: level_3_pass_manager .. _stage_generators: Stage Generator Functions ------------------------- -.. autosummary:: - :toctree: ../stubs/ - - ~qiskit.transpiler.preset_passmanagers.common.generate_control_flow_options_check - ~qiskit.transpiler.preset_passmanagers.common.generate_error_on_control_flow - ~qiskit.transpiler.preset_passmanagers.common.generate_unroll_3q - ~qiskit.transpiler.preset_passmanagers.common.generate_embed_passmanager - ~qiskit.transpiler.preset_passmanagers.common.generate_routing_passmanager - ~qiskit.transpiler.preset_passmanagers.common.generate_pre_op_passmanager - ~qiskit.transpiler.preset_passmanagers.common.generate_translation_passmanager - ~qiskit.transpiler.preset_passmanagers.common.generate_scheduling +.. currentmodule:: qiskit.transpiler.preset_passmanagers.common +.. autofunction:: generate_control_flow_options_check +.. autofunction:: generate_error_on_control_flow +.. autofunction:: generate_unroll_3q +.. autofunction:: generate_embed_passmanager +.. autofunction:: generate_routing_passmanager +.. autofunction:: generate_pre_op_passmanager +.. autofunction:: generate_translation_passmanager +.. autofunction:: generate_scheduling +.. currentmodule:: qiskit.transpiler.preset_passmanagers """ from qiskit.transpiler.passmanager_config import PassManagerConfig diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index 83d66ceae6a2..53fd7b8389bf 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -160,8 +160,9 @@ def pass_manager(self, pass_manager_config, optimization_level): PassManagerStagePlugin PassManagerStagePluginManager - list_stage_plugins - passmanager_stage_plugins + +.. autofunction:: list_stage_plugins +.. autofunction:: passmanager_stage_plugins """ import abc diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index 8d06dd5f48d1..a1329457f8f9 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -18,34 +18,27 @@ .. currentmodule:: qiskit.utils -.. autosummary:: - :toctree: ../stubs/ - - add_deprecation_to_docstring - deprecate_arg - deprecate_arguments - deprecate_func - deprecate_function - local_hardware_info - is_main_process - apply_prefix - detach_prefix - wrap_method +.. autofunction:: add_deprecation_to_docstring +.. autofunction:: deprecate_arg +.. autofunction:: deprecate_arguments +.. autofunction:: deprecate_func +.. autofunction:: deprecate_function +.. autofunction:: local_hardware_info +.. autofunction:: is_main_process +.. autofunction:: apply_prefix +.. autofunction:: detach_prefix +.. autofunction:: wrap_method Algorithm Utilities =================== -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - summarize_circuits - get_entangler_map - validate_entangler_map - has_ibmq - has_aer - name_args - algorithm_globals +.. autofunction:: summarize_circuits +.. autofunction:: get_entangler_map +.. autofunction:: validate_entangler_map +.. autofunction:: has_ibmq +.. autofunction:: has_aer +.. autofunction:: name_args +.. autodata:: algorithm_globals .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/visualization/__init__.py b/qiskit/visualization/__init__.py index 539714f04f19..29eb5cf2c0cc 100644 --- a/qiskit/visualization/__init__.py +++ b/qiskit/visualization/__init__.py @@ -260,10 +260,7 @@ Exceptions ========== -.. autosummary:: - :toctree: ../stubs/ - - VisualizationError +.. autoexception:: VisualizationError """ import os From 1a877037cc8e5497372857203026adea444ed8c8 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Jul 2023 14:06:49 +0100 Subject: [PATCH 13/19] Add tests of `Expr` support through `SabreSwap` (#10511) This also fixes a small bug with the general control-flow handling, where a `SwitchCaseOp` would not have its classical wires respected, which left it possible to be routed out-of-order. --- .../transpiler/passes/routing/sabre_swap.py | 17 +- test/python/transpiler/test_sabre_swap.py | 171 +++++++++++++++++- 2 files changed, 183 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index af7175e2bca1..ea669aba0bed 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -17,8 +17,9 @@ import rustworkx -from qiskit.circuit import ControlFlowOp +from qiskit.circuit import SwitchCaseOp, ControlFlowOp, Clbit, ClassicalRegister from qiskit.circuit.library.standard_gates import SwapGate +from qiskit.circuit.controlflow import condition_resources, node_resources from qiskit.converters import dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap @@ -285,10 +286,18 @@ def process_dag(block_dag, wire_map): dag_list = [] node_blocks = {} for node in block_dag.topological_op_nodes(): - cargs = {block_dag.find_bit(x).index for x in node.cargs} + cargs_bits = set(node.cargs) if node.op.condition is not None: - for clbit in block_dag._bits_in_operation(node.op): - cargs.add(block_dag.find_bit(clbit).index) + cargs_bits.update(condition_resources(node.op.condition).clbits) + if isinstance(node.op, SwitchCaseOp): + target = node.op.target + if isinstance(target, Clbit): + cargs_bits.add(target) + elif isinstance(target, ClassicalRegister): + cargs_bits.update(target) + else: # Expr + cargs_bits.update(node_resources(target).clbits) + cargs = {block_dag.find_bit(x).index for x in cargs_bits} if isinstance(node.op, ControlFlowOp): node_blocks[node._node_id] = [ recurse( diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 9b4ee00dfef8..39ca93628a75 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -18,8 +18,9 @@ import ddt import numpy.random -from qiskit.circuit import Clbit, ControlFlowOp +from qiskit.circuit import Clbit, ControlFlowOp, Qubit from qiskit.circuit.library import CCXGate, HGate, Measure, SwapGate +from qiskit.circuit.classical import expr from qiskit.circuit.random import random_circuit from qiskit.compiler.transpiler import transpile from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -730,6 +731,44 @@ def test_pre_intra_post_if_else(self): expected.measure(qreg, creg[[1, 2, 0, 3, 4]]) self.assertEqual(dag_to_circuit(cdag), expected) + def test_if_expr(self): + """Test simple if conditional with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + body = QuantumCircuit(4) + body.cx(0, 1) + body.cx(0, 2) + body.cx(0, 3) + qc = QuantumCircuit(4, 2) + qc.if_test(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = SabreSwap(coupling, "lookahead", seed=58, trials=1).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + + def test_if_else_expr(self): + """Test simple if/else conditional with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + true = QuantumCircuit(4) + true.cx(0, 1) + true.cx(0, 2) + true.cx(0, 3) + false = QuantumCircuit(4) + false.cx(3, 0) + false.cx(3, 1) + false.cx(3, 2) + qc = QuantumCircuit(4, 2) + qc.if_else(expr.logic_and(qc.clbits[0], qc.clbits[1]), true, false, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = SabreSwap(coupling, "lookahead", seed=58, trials=1).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_no_layout_change(self): """test controlflow with no layout change needed""" num_qubits = 5 @@ -841,6 +880,54 @@ def test_while_loop(self): expected.measure(qreg, creg) self.assertEqual(dag_to_circuit(cdag), expected) + def test_while_loop_expr(self): + """Test simple while loop with an `Expr` condition.""" + coupling = CouplingMap.from_line(4) + + body = QuantumCircuit(4) + body.cx(0, 1) + body.cx(0, 2) + body.cx(0, 3) + qc = QuantumCircuit(4, 2) + qc.while_loop(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], []) + + dag = circuit_to_dag(qc) + cdag = SabreSwap(coupling, "lookahead", seed=82, trials=1).run(dag) + check_map_pass = CheckMap(coupling) + check_map_pass.run(cdag) + self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + + def test_switch_implicit_carg_use(self): + """Test that a switch statement that uses cargs only implicitly via its ``target`` attribute + and not explicitly in bodies of the cases is routed correctly, with the dependencies + fulfilled correctly.""" + coupling = CouplingMap.from_line(4) + pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1) + + body = QuantumCircuit([Qubit()]) + body.x(0) + + # If the classical wire condition isn't respected, then the switch would appear in the front + # layer and be immediately eligible for routing, which would produce invalid output. + qc = QuantumCircuit(4, 1) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(0, 2) + qc.measure(2, 0) + qc.switch(expr.lift(qc.clbits[0]), [(False, body.copy()), (True, body.copy())], [3], []) + + expected = QuantumCircuit(4, 1) + expected.cx(0, 1) + expected.cx(1, 2) + expected.swap(2, 1) + expected.cx(0, 1) + expected.measure(1, 0) + expected.switch( + expr.lift(expected.clbits[0]), [(False, body.copy()), (True, body.copy())], [3], [] + ) + + self.assertEqual(pass_(qc), expected) + def test_switch_single_case(self): """Test routing of 'switch' with just a single case.""" qreg = QuantumRegister(5, "q") @@ -923,6 +1010,88 @@ def test_switch_nonexhaustive(self): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_switch_expr_single_case(self): + """Test routing of 'switch' with an `Expr` target and just a single case.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(3, "c") + qc = QuantumCircuit(qreg, creg) + + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + qc.switch(expr.bit_or(creg, 5), [(0, case0)], qreg[[0, 1, 2]], creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + case0.swap(0, 1) + expected.switch(expr.bit_or(creg, 5), [(0, case0)], qreg[[0, 1, 2]], creg[:]) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + def test_switch_expr_nonexhaustive(self): + """Test routing of 'switch' with an `Expr` target and several but nonexhaustive cases.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(3, "c") + + qc = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.cx(3, 1) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.cx(4, 2) + qc.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + case0.swap(0, 1) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.swap(1, 2) + case1.cx(3, 2) + case1.swap(1, 2) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.swap(2, 3) + case2.cx(4, 3) + case2.swap(2, 3) + expected.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_nested_inner_cnot(self): """test swap in nested if else controlflow construct; swap in inner""" num_qubits = 3 From bf1ff2af81efef6ea7dd3f9977dff51ffe2e21fa Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Jul 2023 14:29:34 +0100 Subject: [PATCH 14/19] Add transpiler integration tests for `Expr` nodes (#10512) * Add transpiler integration tests for `Expr` nodes These tests are the culmination of all the `Expr` work; there was no clean point to add them until all the moving pieces were in place. * Add test skip for O3 on Windows This is the same skip in place as in the related QPY round-trip serialisation, while we work out the issues with eigensystem solver stability, especially on Windows. --- test/python/compiler/test_transpiler.py | 105 ++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 614dad48722d..307e90d914c4 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -39,6 +39,7 @@ SwitchCaseOp, WhileLoopOp, ) +from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay from qiskit.circuit.library import ( CXGate, @@ -1722,6 +1723,42 @@ def _control_flow_circuit(self): base.append(CustomCX(), [3, 4]) return base + def _control_flow_expr_circuit(self): + a = Parameter("a") + regs = [ + QuantumRegister(2, name="q0"), + QuantumRegister(3, name="q1"), + ClassicalRegister(2, name="c0"), + ] + bits = [Qubit(), Qubit(), Clbit()] + base = QuantumCircuit(*regs, bits) + base.h(0) + base.measure(0, 0) + with base.if_test(expr.equal(base.cregs[0], 1)) as else_: + base.cx(0, 1) + base.cz(0, 2) + base.cz(0, 3) + with else_: + base.cz(1, 4) + with base.for_loop((1, 2)): + base.cx(1, 5) + base.measure(2, 2) + with base.while_loop(expr.logic_not(bits[2])): + base.append(CustomCX(), [3, 6]) + base.append(CustomCX(), [5, 4]) + base.append(CustomCX(), [5, 3]) + base.append(CustomCX(), [2, 4]) + base.ry(a, 4) + base.measure(4, 2) + with base.switch(expr.bit_and(base.cregs[0], 2)) as case_: + with case_(0, 1): + base.cz(3, 5) + with case_(case_.DEFAULT): + base.cz(1, 4) + base.append(CustomCX(), [2, 4]) + base.append(CustomCX(), [3, 4]) + return base + @data(0, 1, 2, 3) def test_qpy_roundtrip(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" @@ -1808,6 +1845,51 @@ def test_qpy_roundtrip_control_flow_backendv2(self, optimization_level): round_tripped = qpy.load(buffer)[0] self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) + def test_qpy_roundtrip_control_flow_expr(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including `Expr` nodes can + be round-tripped through QPY.""" + if optimization_level == 3 and sys.platform == "win32": + self.skipTest( + "This test case triggers a bug in the eigensolver routine on windows. " + "See #10345 for more details." + ) + backend = FakeMelbourne() + transpiled = transpile( + self._control_flow_expr_circuit(), + backend=backend, + basis_gates=backend.configuration().basis_gates + + ["if_else", "for_loop", "while_loop", "switch_case"], + optimization_level=optimization_level, + seed_transpiler=2023_07_26, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + + @data(0, 1, 2, 3) + def test_qpy_roundtrip_control_flow_expr_backendv2(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including `Expr` nodes can + be round-tripped through QPY.""" + backend = FakeMumbaiV2() + backend.target.add_instruction(IfElseOp, name="if_else") + backend.target.add_instruction(ForLoopOp, name="for_loop") + backend.target.add_instruction(WhileLoopOp, name="while_loop") + backend.target.add_instruction(SwitchCaseOp, name="switch_case") + transpiled = transpile( + self._control_flow_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2023_07_26, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) def test_qasm3_output(self, optimization_level): """Test that the output of a transpiled circuit can be dumped into OpenQASM 3.""" @@ -1845,6 +1927,29 @@ def test_qasm3_output_control_flow(self, optimization_level): str, ) + @data(0, 1, 2, 3) + def test_qasm3_output_control_flow_expr(self, optimization_level): + """Test that the output of a transpiled circuit with control flow and `Expr` nodes can be + dumped into OpenQASM 3.""" + backend = FakeMumbaiV2() + backend.target.add_instruction(IfElseOp, name="if_else") + backend.target.add_instruction(ForLoopOp, name="for_loop") + backend.target.add_instruction(WhileLoopOp, name="while_loop") + backend.target.add_instruction(SwitchCaseOp, name="switch_case") + transpiled = transpile( + self._control_flow_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2023_07_26, + ) + # TODO: There's not a huge amount we can sensibly test for the output here until we can + # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump + # itself doesn't throw an error, though. + self.assertIsInstance( + qasm3.dumps(transpiled, experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1).strip(), + str, + ) + @data(0, 1, 2, 3) def test_transpile_target_no_measurement_error(self, opt_level): """Test that transpile with a target which contains ideal measurement works From 9ae6164323308f1bd95c67257ce6c4e9722709ef Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Jul 2023 14:45:30 +0100 Subject: [PATCH 15/19] Add release note for `Expr` support (#10503) * Add release note for `Expr` support * Fix role typo --- ...pr-rvalue-conditions-8b5d5f7c015658c0.yaml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 releasenotes/notes/expr-rvalue-conditions-8b5d5f7c015658c0.yaml diff --git a/releasenotes/notes/expr-rvalue-conditions-8b5d5f7c015658c0.yaml b/releasenotes/notes/expr-rvalue-conditions-8b5d5f7c015658c0.yaml new file mode 100644 index 000000000000..234888b2ac9b --- /dev/null +++ b/releasenotes/notes/expr-rvalue-conditions-8b5d5f7c015658c0.yaml @@ -0,0 +1,83 @@ +--- +features: + - | + The fields :attr:`.IfElseOp.condition`, :attr:`.WhileLoopOp.condition` and + :attr:`.SwitchCaseOp.target` can now be instances of the new runtime classical-expression type + :class:`.expr.Expr`. This is distinct from :class:`.ParameterExpression` because it is + evaluated *at runtime* for backends that support such operations. + + These new expressions have significantly more power than the old two-tuple form of supplying + classical conditions. For example, one can now represent equality constraints between two + different classical registers, or the logic "or" of two classical bits. These two examples + would look like:: + + from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister + from qiskit.circuit.classical import expr + + qr = QuantumRegister(4) + cr1 = ClassicalRegister(2) + cr2 = ClassicalRegister(2) + qc = QuantumCircuit(qr, cr1, cr2) + qc.h(0) + qc.cx(0, 1) + qc.h(2) + qc.cx(2, 3) + qc.measure([0, 1, 2, 3], [0, 1, 2, 3]) + + # If the two registers are equal to each other. + with qc.if_test(expr.equal(cr1, cr2)): + qc.x(0) + + # While either of two bits are set. + with qc.while_loop(expr.logic_or(cr1[0], cr1[1])): + qc.reset(0) + qc.reset(1) + qc.measure([0, 1], cr1) + + For more examples, see the documentation for :mod:`qiskit.circuit.classical`. + + This is a feature that hardware is only just beginning to support, so Qiskit's support is + starting conservatively, and you may well find rough edges in places where hardware doesn't + support expressions that look simple in Qiskit. Please bear with us, and your hardware vendors! + + In this initial release, Qiskit has added the operations: + + * :func:`~.expr.bit_not` + * :func:`~.expr.logic_not` + * :func:`~.expr.bit_and` + * :func:`~.expr.bit_or` + * :func:`~.expr.bit_xor` + * :func:`~.expr.logic_and` + * :func:`~.expr.logic_or` + * :func:`~.expr.equal` + * :func:`~.expr.not_equal` + * :func:`~.expr.less` + * :func:`~.expr.less_equal` + * :func:`~.expr.greater` + * :func:`~.expr.greater_equal` + + These can act on Python integer and Boolean literals, or on :class:`.ClassicalRegister` + and :class:`.Clbit` instances. + + All these classical expressions are fully supported through the Qiskit transpiler stack, through + QPY serialisation (:mod:`qiskit.qpy`) and for export to OpenQASM 3 (:mod:`qiskit.qasm3`). Import + from OpenQASM 3 is currently managed by `a separate package `__ + (which is re-exposed via :mod:`qiskit.qasm3`), which we hope will be extended to match the new + features in Qiskit. + - | + In addition to the new representations of classical runtime expressions, some tools for working + with these objects have also been added. A general :class:`~.expr.ExprVisitor` is provided for + consumers of these expressions to subclass. Two utilities based on this structure, + :func:`~.expr.iter_vars` and :func:`~.expr.structurally_equivalent`, are also provided, which + respectively produce an iterator through the :class:`~.expr.Var` nodes and check whether two + :class:`~.expr.Expr` instances are structurally the same, up to some mapping of the + :class:`~.expr.Var` nodes contained. + - | + For those who wish to convert all old-style conditions into new-style :class:`~.expr.Expr` nodes + immediately, the function :func:`~.expr.lift_legacy_condition` is provided, though these new + expression nodes are not permitted in old-style :attr:`.Instruction.condition` fields, which are + due to be replaced by more advanced classical handling such as :class:`.IfElseOp`. +issues: + - | + Circuits containing classical expressions made with the :mod:`~.classical.expr` module are not + yet supported by the circuit visualizers. From c8552f6b51a36aa432b21b0d31b88e212a104ca7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 27 Jul 2023 16:59:40 +0100 Subject: [PATCH 16/19] Make circuit drawers *not crash* on `Expr` nodes (#10504) * Make circuit drawers *not crash* on `Expr` nodes This at least causes the circuit visualisers to not crash when encountering an `Expr` node, and instead emit a warning and make a best-effort attempt (except for LaTeX) to output _something_. We intend to extend the capabilities of these drawers in the future. * Soften warnings about unsupported `Expr` nodes --- qiskit/circuit/quantumcircuit.py | 7 ++ qiskit/visualization/circuit/_utils.py | 16 ++-- .../circuit/circuit_visualization.py | 6 ++ qiskit/visualization/circuit/latex.py | 7 +- qiskit/visualization/circuit/matplotlib.py | 76 ++++++++++++------- qiskit/visualization/circuit/text.py | 24 ++++++ 6 files changed, 98 insertions(+), 38 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 1d99ab34e679..841d09b1b6bc 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1794,6 +1794,13 @@ def draw( **latex_source**: raw uncompiled latex output. + .. warning:: + + Support for :class:`~.expr.Expr` nodes in conditions and :attr:`.SwitchCaseOp.target` + fields is preliminary and incomplete. The ``text`` and ``mpl`` drawers will make a + best-effort attempt to show data dependencies, but the LaTeX-based drawers will skip + these completely. + Args: output (str): select the output method to use for drawing the circuit. Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``. diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index 883e9cb6527c..3d8d5dc79edb 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -25,6 +25,7 @@ Instruction, Measure, ) +from qiskit.circuit.controlflow import condition_resources from qiskit.circuit.library import PauliEvolutionGate from qiskit.circuit import ClassicalRegister, QuantumCircuit, Qubit, ControlFlowOp from qiskit.circuit.tools import pi_check @@ -549,16 +550,11 @@ def slide_from_left(self, node, index): curr_index = index last_insertable_index = -1 index_stop = -1 - if getattr(node.op, "condition", None): - if isinstance(node.op.condition[0], Clbit): - cond_bit = [clbit for clbit in self.clbits if node.op.condition[0] == clbit] - index_stop = self.measure_map[cond_bit[0]] - else: - for bit in node.op.condition[0]: - max_index = -1 - if bit in self.measure_map: - if self.measure_map[bit] > max_index: - index_stop = max_index = self.measure_map[bit] + if (condition := getattr(node.op, "condition", None)) is not None: + index_stop = max( + (self.measure_map[bit] for bit in condition_resources(condition).clbits), + default=index_stop, + ) if node.cargs: for carg in node.cargs: try: diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index be5d2c423d7b..0ba53fea64c3 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -73,6 +73,12 @@ def circuit_drawer( **latex_source**: raw uncompiled latex output. + .. warning:: + + Support for :class:`~.expr.Expr` nodes in conditions and :attr:`.SwitchCaseOp.target` fields + is preliminary and incomplete. The ``text`` and ``mpl`` drawers will make a best-effort + attempt to show data dependencies, but the LaTeX-based drawers will skip these completely. + Args: circuit (QuantumCircuit): the quantum circuit to draw scale (float): scale of image to draw (shrink if < 1.0). Only used by diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index f7743f59ad88..67a6afe387c8 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -20,6 +20,7 @@ import numpy as np from qiskit.circuit import Clbit, Qubit, ClassicalRegister, QuantumRegister, QuantumCircuit +from qiskit.circuit.classical import expr from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.library.standard_gates import SwapGate, XGate, ZGate, RZZGate, U1Gate, PhaseGate from qiskit.circuit.measure import Measure @@ -416,7 +417,10 @@ def _build_latex_array(self): num_cols_op = 1 wire_list = [self._wire_map[qarg] for qarg in node.qargs if qarg in self._qubits] if getattr(op, "condition", None): - self._add_condition(op, wire_list, column) + if isinstance(op.condition, expr.Expr): + warn("ignoring expression condition, which is not supported yet") + else: + self._add_condition(op, wire_list, column) if isinstance(op, Measure): self._build_measure(node, column) @@ -619,7 +623,6 @@ def _add_condition(self, op, wire_list, col): # cwire - the wire number for the first wire for the condition register # or if cregbundle, wire number of the condition register itself # gap - the number of wires from cwire to the bottom gate qubit - label, val_bits = get_condition_label_val(op.condition, self._circuit, self._cregbundle) cond_is_bit = isinstance(op.condition[0], Clbit) cond_reg = op.condition[0] diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 2c64ad0e4a47..fb61c7cf6d3b 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -14,6 +14,7 @@ """mpl circuit visualization backend.""" +import collections import itertools import re from warnings import warn @@ -33,6 +34,8 @@ ForLoopOp, SwitchCaseOp, ) +from qiskit.circuit.controlflow import condition_resources +from qiskit.circuit.classical import expr from qiskit.circuit.library.standard_gates import ( SwapGate, RZZGate, @@ -1090,45 +1093,66 @@ def _condition(self, node, node_data, wire_map, cond_xy, glob_data): # 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): + if isinstance(node.op.target, expr.Expr): + condition = node.op.target + elif 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]) + override_fc = False first_clbit = len(self._qubits) cond_pos = [] - # In the first case, multiple bits are indicated on the drawing. In all - # other cases, only one bit is shown. - if not self._cregbundle and isinstance(cond_bit_reg, ClassicalRegister): - for idx in range(cond_bit_reg.size): - cond_pos.append(cond_xy[wire_map[cond_bit_reg[idx]] - first_clbit]) - - # If it's a register bit and cregbundle, need to use the register to find the location - elif self._cregbundle and isinstance(cond_bit_reg, Clbit): - register = get_bit_register(self._circuit, cond_bit_reg) - if register is not None: - cond_pos.append(cond_xy[wire_map[register] - first_clbit]) + if isinstance(condition, expr.Expr): + # If fixing this, please update the docstrings of `QuantumCircuit.draw` and + # `visualization.circuit_drawer` to remove warnings. + condition_bits = condition_resources(condition).clbits + label = "[expression]" + override_fc = True + registers = collections.defaultdict(list) + for bit in condition_bits: + registers[get_bit_register(self._circuit, bit)].append(bit) + # Registerless bits don't care whether cregbundle is set. + cond_pos.extend(cond_xy[wire_map[bit] - first_clbit] for bit in registers.pop(None, ())) + if self._cregbundle: + cond_pos.extend( + cond_xy[wire_map[register[0]] - first_clbit] for register in registers + ) else: - cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) + cond_pos.extend( + cond_xy[wire_map[bit] - first_clbit] + for register, bits in registers.items() + for bit in bits + ) + val_bits = ["1"] * len(cond_pos) else: - cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) + label, val_bits = get_condition_label_val(condition, self._circuit, self._cregbundle) + cond_bit_reg = condition[0] + cond_bit_val = int(condition[1]) + override_fc = cond_bit_val != 0 + + # In the first case, multiple bits are indicated on the drawing. In all + # other cases, only one bit is shown. + if not self._cregbundle and isinstance(cond_bit_reg, ClassicalRegister): + for idx in range(cond_bit_reg.size): + cond_pos.append(cond_xy[wire_map[cond_bit_reg[idx]] - first_clbit]) + + # If it's a register bit and cregbundle, need to use the register to find the location + elif self._cregbundle and isinstance(cond_bit_reg, Clbit): + register = get_bit_register(self._circuit, cond_bit_reg) + if register is not None: + cond_pos.append(cond_xy[wire_map[register] - first_clbit]) + else: + cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) + else: + cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) xy_plot = [] - for idx, xy in enumerate(cond_pos): - if val_bits[idx] == "1" or ( - isinstance(cond_bit_reg, ClassicalRegister) - and cond_bit_val != 0 - and self._cregbundle - ): - fc = self._style["lc"] - else: - fc = self._style["bg"] + for val_bit, xy in zip(val_bits, cond_pos): + fc = self._style["lc"] if override_fc or val_bit == "1" else self._style["bg"] box = glob_data["patches_mod"].Circle( xy=xy, radius=WID * 0.15, diff --git a/qiskit/visualization/circuit/text.py b/qiskit/visualization/circuit/text.py index 3d88a5c88c87..0185723fc8c6 100644 --- a/qiskit/visualization/circuit/text.py +++ b/qiskit/visualization/circuit/text.py @@ -16,6 +16,7 @@ from warnings import warn from shutil import get_terminal_size +import collections import itertools import sys @@ -23,6 +24,8 @@ from qiskit.circuit import ControlledGate from qiskit.circuit import Reset from qiskit.circuit import Measure +from qiskit.circuit.classical import expr +from qiskit.circuit.controlflow import node_resources from qiskit.circuit.library.standard_gates import IGate, RZZGate, SwapGate, SXGate, SXdgGate from qiskit.circuit.tools.pi_check import pi_check @@ -1344,6 +1347,27 @@ def set_cl_multibox(self, condition, top_connect="┴"): Returns: List: list of tuples of connections between clbits for multi-bit conditions """ + if isinstance(condition, expr.Expr): + # If fixing this, please update the docstrings of `QuantumCircuit.draw` and + # `visualization.circuit_drawer` to remove warnings. + label = "" + out = [] + condition_bits = node_resources(condition).clbits + registers = collections.defaultdict(list) + for bit in condition_bits: + registers[get_bit_register(self._circuit, bit)].append(bit) + if registerless := registers.pop(None, ()): + out.extend(self.set_cond_bullets(label, ["1"] * len(registerless), registerless)) + if self.cregbundle: + # It's hard to do something properly sensible here without more major rewrites, so + # as a minimum to *not crash* we'll just treat a condition that touches part of a + # register like it touched the whole register. + for register in registers: + self.set_clbit(register[0], BoxOnClWire(label=label, top_connect=top_connect)) + else: + for register, bits in registers.items(): + out.extend(self.set_cond_bullets(label, ["1"] * len(bits), bits)) + return out label, val_bits = get_condition_label_val(condition, self._circuit, self.cregbundle) if isinstance(condition[0], ClassicalRegister): cond_reg = condition[0] From 4722c50a59157a5be638c8b30a2b77a0b127e4ae Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 31 Jul 2023 17:05:13 +0100 Subject: [PATCH 17/19] Force decimal points in OpenQASM 2 floats (#10532) * Force decimal points in OpenQASM 2 floats The letter of the OpenQASM 2 specification has a regex defining floating-point literals that requires a decimal point, even in the presence of an exponential component. This is unusual for most programming languages, but Qiskit's exporter should follow the spec as close as we can to increase interop. Our parser accepts floats with an exponent and no decimal point as a minor syntax extension, unless in strict mode. * Simplify English --- qiskit/circuit/tools/pi_check.py | 11 +++++------ .../qasm2-float-decimal-76b44281d9249f7a.yaml | 8 ++++++++ test/python/circuit/test_circuit_qasm.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/qasm2-float-decimal-76b44281d9249f7a.yaml diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py index a55249d7d181..d3614b747824 100644 --- a/qiskit/circuit/tools/pi_check.py +++ b/qiskit/circuit/tools/pi_check.py @@ -167,12 +167,11 @@ def normalize(single_inpt): str_out = f"{neg_str}{numer}/{denom}{pi}" return str_out - # Nothing found - if ndigits is None: - str_out = "{}".format(single_inpt) - else: - str_out = "{:.{}g}".format(single_inpt, ndigits) - return str_out + # Nothing found. The '#' forces a decimal point to be included, which OQ2 needs, but other + # formats don't really. + if output == "qasm": + return f"{single_inpt:#}" if ndigits is None else f"{single_inpt:#.{ndigits}g}" + return f"{single_inpt}" if ndigits is None else f"{single_inpt:.{ndigits}g}" complex_inpt = complex(inpt) real, imag = map(normalize, [complex_inpt.real, complex_inpt.imag]) diff --git a/releasenotes/notes/qasm2-float-decimal-76b44281d9249f7a.yaml b/releasenotes/notes/qasm2-float-decimal-76b44281d9249f7a.yaml new file mode 100644 index 000000000000..03fd561324d7 --- /dev/null +++ b/releasenotes/notes/qasm2-float-decimal-76b44281d9249f7a.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Angles in the OpenQASM 2 exporter (:func:`.QuantumCircuit.qasm`) will now always include a + decimal point, for example in the case of ``1.e-5``. This is required by a strict interpretation of the + floating-point-literal specification in OpenQASM 2. Qiskit's OpenQASM 2 parser + (:func:`.qasm2.load` and :func:`~.qasm2.loads`) is more permissive by default, and will allow + ``1e-5`` without the decimal point unless in ``strict`` mode. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 0aa6ac6ececc..9b1fca492d5e 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -843,6 +843,20 @@ def test_empty_barrier(self): qreg qr1[2]; qreg qr2[3]; barrier qr1[0],qr1[1],qr2[0],qr2[1],qr2[2]; +""" + self.assertEqual(qc.qasm(), expected) + + def test_small_angle_valid(self): + """Test that small angles do not get converted to invalid OQ2 floating-point values.""" + # OQ2 _technically_ requires a decimal point in all floating-point values, even ones that + # are followed by an exponent. + qc = QuantumCircuit(1) + qc.rx(0.000001, 0) + expected = """\ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +rx(1.e-06) q[0]; """ self.assertEqual(qc.qasm(), expected) From e0ca5f0711bf84102726b92bc68ef39a5384c2d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 20:36:49 +0000 Subject: [PATCH 18/19] Bump rustworkx-core from 0.13.0 to 0.13.1 (#10538) Bumps [rustworkx-core](https://github.com/Qiskit/rustworkx) from 0.13.0 to 0.13.1. - [Release notes](https://github.com/Qiskit/rustworkx/releases) - [Commits](https://github.com/Qiskit/rustworkx/compare/0.13.0...0.13.1) --- updated-dependencies: - dependency-name: rustworkx-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 865c336cf0b7..fbc8e30e501b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -552,9 +552,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustworkx-core" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e9d46abff559cde170ba3bca60c58698356053a395419d29a26f512df68f88" +checksum = "34932d9b2a5d67da9f166a70009fc742075a5844671c5f000b89dec578e0f024" dependencies = [ "ahash 0.8.3", "fixedbitset", From 28113b6a9fc1fd31bc211a73fe7a6908fffa1114 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 13:48:14 +0100 Subject: [PATCH 19/19] Bump pyo3 from 0.19.1 to 0.19.2 (#10543) Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.19.1 to 0.19.2. - [Release notes](https://github.com/pyo3/pyo3/releases) - [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyo3/pyo3/compare/v0.19.1...v0.19.2) --- updated-dependencies: - dependency-name: pyo3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++++++++++---------- crates/accelerate/Cargo.toml | 2 +- crates/qasm2/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbc8e30e501b..04112444d3cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb88ae05f306b4bfcde40ac4a51dc0b05936a9207a4b75b798c7729c4258a59" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" dependencies = [ "cfg-if", "hashbrown", @@ -370,9 +370,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" dependencies = [ "once_cell", "target-lexicon", @@ -380,9 +380,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922ede8759e8600ad4da3195ae41259654b9c55da4f7eec84a0ccc7d067a70a4" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" dependencies = [ "libc", "pyo3-build-config", @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5caec6a1dd355964a841fcbeeb1b89fe4146c87295573f94228911af3cc5a2" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b78ccbb160db1556cdb6fd96c50334c5d4ec44dc5e0a968d0a1208fa0efa8b" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" dependencies = [ "proc-macro2", "quote", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 807b9d145f18..4e83e718dff3 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -25,7 +25,7 @@ rustworkx-core = "0.13" # The base version of PyO3 and setting a minimum feature set (e.g. probably just 'extension-module') # can be done in the workspace and inherited once we hit Rust 1.64. [dependencies.pyo3] -version = "0.19.1" +version = "0.19.2" features = ["extension-module", "hashbrown", "indexmap", "num-complex", "num-bigint", "abi3-py38"] [dependencies.ndarray] diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 0552d5126aca..3aea21ad2587 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] [dependencies] hashbrown = "0.12.3" -pyo3 = { version = "0.19.1", features = ["extension-module", "abi3-py38"] } +pyo3 = { version = "0.19.2", features = ["extension-module", "abi3-py38"] }
SoftwareVersion
{name}{version}
{name}{version}
System information