From d58e6ff5bf7810be516aab6ebdddf08072b39c7f Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Mon, 30 Sep 2024 12:16:04 +0300 Subject: [PATCH 1/8] code from private developer branch --- .../passes/synthesis/high_level_synthesis.py | 398 ++++++++++++++---- .../passes/synthesis/qubit_tracker.py | 193 ++++----- ...e-hls-qubit-tracking-6b6288d556e3af9d.yaml | 8 + .../transpiler/test_high_level_synthesis.py | 132 ++++++ 4 files changed, 537 insertions(+), 194 deletions(-) create mode 100644 releasenotes/notes/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 733d5465fd0a..171e6f83ca63 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -28,7 +28,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence +from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence, Qubit from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap @@ -135,6 +135,58 @@ def set_methods(self, hls_name, hls_methods): self.methods[hls_name] = hls_methods +class QubitContext: + """Correspondence between local qubits and global qubits. + + An internal class for handling recursion within HighLevelSynthesis. + Provides the correspondence between the qubit indices of an internal DAG, + aka the "local qubits" (for instance, of the definition circuit + of a custom gate), and the qubit indices of the original DAG, aka the + "global qubits". + + The local qubit indices are assumed to be consecutive integers starting at + zero, i.e. 0, 1, 2, etc. + """ + + def __init__(self, local_to_global: dict): + self.local_to_global = local_to_global + + def num_qubits(self) -> int: + """Returns the number of local qubits.""" + return len(self.local_to_global) + + def add_qubit(self, global_qubit) -> int: + """Extends the correspondence by an additional qubit that + maps to the given global qubit. Returns the index of the + new local qubit. + """ + new_local_qubit = len(self.local_to_global) + self.local_to_global[new_local_qubit] = global_qubit + return new_local_qubit + + def to_global_mapping(self): + """Returns the local-to-global mapping.""" + return self.local_to_global + + def to_local_mapping(self): + """Returns the global-to-local mapping.""" + return {j: i for (i, j) in self.local_to_global.items()} + + def restrict(self, qubits: list[int] | tuple[int]) -> "QubitContext": + """Restricts the context to a subset of qubits, remapping the indices as + to be consecutive integers starting at zero. + """ + return QubitContext({i: self.local_to_global[qubits[i]] for i in range(len(qubits))}) + + def to_global(self, qubit: int) -> int: + """Returns the global qubits corresponding to the given local qubits.""" + return self.local_to_global[qubit] + + def to_globals(self, qubits: list[int]) -> list[int]: + """Returns the global qubits corresponding to the given local qubits.""" + return [self.local_to_global[q] for q in qubits] + + class HighLevelSynthesis(TransformationPass): r"""Synthesize higher-level objects and unroll custom definitions. @@ -271,96 +323,160 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ qubits = tuple(dag.find_bit(q).index for q in dag.qubits) + context = QubitContext({i: i for i in range(len(dag.qubits))}) + tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.qubits_initially_zero: - clean, dirty = set(qubits), set() - else: - clean, dirty = set(), set(qubits) + tracker.set_clean(context.to_globals(qubits)) - tracker = QubitTracker(qubits=qubits, clean=clean, dirty=dirty) - return self._run(dag, tracker) + out_dag = self._run(dag, tracker, context, use_ancillas=True, top_level=True) + return out_dag - def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: - # Check if HighLevelSynthesis can be skipped. - for node in dag.op_nodes(): - qubits = tuple(dag.find_bit(q).index for q in node.qargs) - if not self._definitely_skip_node(node, qubits, dag): - break - else: - # The for-loop terminates without reaching the break statement - return dag + def _run( + self, + dag: DAGCircuit, + tracker: QubitTracker, + context: QubitContext, + use_ancillas: bool, + top_level: bool, + ) -> DAGCircuit: + """ + The main recursive function that synthesizes a DAGCircuit. + + Input: + dag: the DAG to be synthesized. + tracker: the global tracker, tracking the state of original qubits. + context: the correspondence between the dag's qubits and the global qubits. + use_ancillas: if True, synthesis algorithms are allowed to use ancillas. + top_level: specifies if this is the top-level of the recursion. - # Start by analyzing the nodes in the DAG. This for-loop is a first version of a potentially - # more elaborate approach to find good operation/ancilla allocations. It greedily iterates - # over the nodes, checking whether we can synthesize them, while keeping track of the - # qubit states. It does not trade-off allocations and just gives all available qubits - # to the current operation (a "the-first-takes-all" approach). + The function returns the synthesized DAG. + + Note that by using the auxiliary qubits to synthesize operations present in the input DAG, + the synthesized DAG may be defined over more qubits than the input DAG. In this case, + the function update in-place the global qubits tracker and extends the local-to-global + context. + """ + + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + + # STEP 1: Check if HighLevelSynthesis can be skipped altogether. This is only + # done at the top-level since this does not update the global qubits tracker. + if top_level: + for node in dag.op_nodes(): + qubits = tuple(dag.find_bit(q).index for q in node.qargs) + if not self._definitely_skip_node(node, qubits, dag): + break + else: + # The for-loop terminates without reaching the break statement + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + return dag + + # STEP 2: Analyze the nodes in the DAG. For each node in the DAG that needs + # to be synthesized, we recursively synthesize it and store the result. For + # instance, the result of synthesizing a custom gate is a DAGCircuit corresponding + # to the (recursively synthesized) gate's definition. When the result is a + # DAG, we also store its context (the mapping of its qubits to global qubits). + # In addition, we keep track of the qubit states using the (global) qubits tracker. + # + # Note: This is a first version of a potentially more elaborate approach to find + # good operation/ancilla allocations. The current approach is greedy and just gives + # all available ancilla qubits to the current operation ("the-first-takes-all" approach). + # It does not distribute ancilla qubits between different operations present in the DAG. synthesized_nodes = {} for node in dag.topological_op_nodes(): qubits = tuple(dag.find_bit(q).index for q in node.qargs) + processed = False synthesized = None - used_qubits = None + synthesized_context = None + + # Start by handling special operations. Other cases can also be + # considered: swaps, automatically simplifying control gate (e.g. if + # a control is 0). + if node.op.name in ["id", "delay", "barrier"]: + # tracker not updated, these are no-ops + processed = True + + elif node.op.name == "reset": + # reset qubits to 0 + tracker.set_clean(context.to_globals(qubits)) + processed = True # check if synthesis for the operation can be skipped - if self._definitely_skip_node(node, qubits, dag): - pass + elif self._definitely_skip_node(node, qubits, dag): + tracker.set_dirty(context.to_globals(qubits)) # next check control flow elif node.is_control_flow(): - dag.substitute_node( - node, - control_flow.map_blocks(partial(self._run, tracker=tracker.copy()), node.op), - propagate_condition=False, + inner_context = context.restrict(qubits) + synthesized = control_flow.map_blocks( + partial( + self._run, + tracker=tracker, + context=inner_context, + use_ancillas=False, + top_level=False, + ), + node.op, ) # now we are free to synthesize else: - # this returns the synthesized operation and the qubits it acts on -- note that this - # may be different from the original qubits, since we may use auxiliary qubits - synthesized, used_qubits = self._synthesize_operation(node.op, qubits, tracker) + # This returns the synthesized operation and its context (when the result is + # a DAG, it's the correspondence between its qubits and the global qubits). + # Also note that the DAG may use auxiliary qubits. The qubits tracker and the + # current DAG's context are updated in-place. + synthesized, synthesized_context = self._synthesize_operation( + node.op, qubits, tracker, context, use_ancillas=use_ancillas + ) - # if the synthesis changed the operation (i.e. it is not None), store the result - # and mark the operation qubits as used + # If the synthesis changed the operation (i.e. it is not None), store the result. if synthesized is not None: - synthesized_nodes[node] = (synthesized, used_qubits) - tracker.used(qubits) # assumes that auxiliary are returned in the same state + synthesized_nodes[node] = (synthesized, synthesized_context) - # if the synthesis did not change anything, just update the qubit tracker - # other cases can be added: swaps, controlled gates (e.g. if control is 0), ... - else: - if node.op.name in ["id", "delay", "barrier"]: - pass # tracker not updated, these are no-ops - elif node.op.name == "reset": - tracker.reset(qubits) # reset qubits to 0 - else: - tracker.used(qubits) # any other op used the clean state up + # If the synthesis did not change anything, just update the qubit tracker. + elif not processed: + tracker.set_dirty(context.to_globals(qubits)) - # we did not change anything just return the input + # We did not change anything just return the input. if len(synthesized_nodes) == 0: + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") return dag - # Otherwise, we will rebuild with the new operations. Note that we could also + # STEP 3. We rebuild the DAG with new operations. Note that we could also # check if no operation changed in size and substitute in-place, but rebuilding is # generally as fast or faster, unless very few operations are changed. out = dag.copy_empty_like() - index_to_qubit = dict(enumerate(dag.qubits)) + num_additional_qubits = context.num_qubits() - out.num_qubits() + + if num_additional_qubits > 0: + out.add_qubits([Qubit() for _ in range(num_additional_qubits)]) + + index_to_qubit = dict(enumerate(out.qubits)) + outer_to_local = context.to_local_mapping() for node in dag.topological_op_nodes(): if node in synthesized_nodes: - op, qubits = synthesized_nodes[node] - qargs = tuple(index_to_qubit[index] for index in qubits) + op, op_context = synthesized_nodes[node] + if isinstance(op, Operation): - out.apply_operation_back(op, qargs, cargs=[]) + out.apply_operation_back(op, node.qargs, node.cargs) continue if isinstance(op, QuantumCircuit): op = circuit_to_dag(op, copy_operations=False) + inner_to_global = op_context.to_global_mapping() if isinstance(op, DAGCircuit): qubit_map = { - qubit: index_to_qubit[index] for index, qubit in zip(qubits, op.qubits) + q: index_to_qubit[outer_to_local[inner_to_global[i]]] + for (i, q) in enumerate(op.qubits) } clbit_map = dict(zip(op.clbits, node.cargs)) + for sub_node in op.op_nodes(): out.apply_operation_back( sub_node.op, @@ -368,11 +484,15 @@ def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: tuple(clbit_map[carg] for carg in sub_node.cargs), ) out.global_phase += op.global_phase + else: - raise RuntimeError(f"Unexpected synthesized type: {type(op)}") + raise TranspilerError(f"Unexpected synthesized type: {type(op)}") else: out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + if out.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + return out def _synthesize_operation( @@ -380,7 +500,23 @@ def _synthesize_operation( operation: Operation, qubits: tuple[int], tracker: QubitTracker, - ) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, list[int] | None]: + context: QubitContext, + use_ancillas: bool, + ) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, QubitContext | None]: + """ + Synthesizes an operation. The function receives the qubits on which the operation + is defined in the current DAG, the correspondence between the qubits of the current + DAG and the global qubits and the global qubits tracker. The function returns the + result of synthesizing the operation. The value of `None` means that the operation + should remain as it is. When it's a circuit, we also return the context, i.e. the + correspondence of its local qubits and the global qubits. The function changes + in-place the tracker (state of the global qubits), the qubits (when the synthesized + operation is defined over additional ancilla qubits), and the context (to keep track + of where these ancilla qubits maps to). + """ + + synthesized_context = None + # Try to synthesize the operation. We'll go through the following options: # (1) Annotations: if the operator is annotated, synthesize the base operation # and then apply the modifiers. Returns a circuit (e.g. applying a power) @@ -389,31 +525,62 @@ def _synthesize_operation( # if the operation is a Clifford). Returns a circuit. # (3) Unrolling custom definitions: try defining the operation if it is not yet # in the set of supported instructions. Returns a circuit. + # # If any of the above were triggered, we will recurse and go again through these steps # until no further change occurred. At this point, we convert circuits to DAGs (the final # possible return type). If there was no change, we just return ``None``. + num_original_qubits = len(qubits) + qubits = list(qubits) + synthesized = None # Try synthesizing via AnnotatedOperation. This is faster than an isinstance check # but a bit less safe since someone could create operations with a ``modifiers`` attribute. if len(modifiers := getattr(operation, "modifiers", [])) > 0: - # The base operation must be synthesized without using potential control qubits + # Note: the base operation must be synthesized without using potential control qubits # used in the modifiers. num_ctrl = sum( mod.num_ctrl_qubits for mod in modifiers if isinstance(mod, ControlModifier) ) baseop_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones - baseop_tracker = tracker.copy(drop=qubits[:num_ctrl]) # no access to control qubits # get qubits of base operation - synthesized_base_op, _ = self._synthesize_operation( - operation.base_op, baseop_qubits, baseop_tracker + control_qubits = qubits[0:num_ctrl] + + # Do not allow access to control qubits + tracker.disable(context.to_globals(control_qubits)) + synthesized_base_op, baseop_data = self._synthesize_operation( + operation.base_op, + baseop_qubits, + tracker, + context, + use_ancillas=use_ancillas, ) + if synthesized_base_op is None: synthesized_base_op = operation.base_op elif isinstance(synthesized_base_op, DAGCircuit): synthesized_base_op = dag_to_circuit(synthesized_base_op) + # Handle the case that synthesizing the base operation introduced + # additional qubits (e.g. the base operation is a circuit that includes + # an MCX gate). + if synthesized_base_op.num_qubits > len(baseop_qubits): + global_aux_qubits = tracker.borrow( + synthesized_base_op.num_qubits - len(baseop_qubits), + context.to_globals(baseop_qubits), + ) + global_to_local = context.to_local_mapping() + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + # Restore access to control qubits. + tracker.enable(context.to_globals(control_qubits)) + + # This step currently does not introduce ancilla qubits. synthesized = self._apply_annotations(synthesized_base_op, operation.modifiers) # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. @@ -421,57 +588,106 @@ def _synthesize_operation( # Try synthesis via HLS -- which will return ``None`` if unsuccessful. indices = qubits if self._use_qubit_indices else None if len(hls_methods := self._methods_to_try(operation.name)) > 0: + if use_ancillas: + num_clean_available = tracker.num_clean(context.to_globals(qubits)) + num_dirty_available = tracker.num_dirty(context.to_globals(qubits)) + else: + num_clean_available = 0 + num_dirty_available = 0 synthesized = self._synthesize_op_using_plugins( hls_methods, operation, indices, - tracker.num_clean(qubits), - tracker.num_dirty(qubits), + num_clean_available, + num_dirty_available, ) + # It may happen that the plugin synthesis method uses clean/dirty ancilla qubits + if (synthesized is not None) and (synthesized.num_qubits > len(qubits)): + # need to borrow more qubits from tracker + global_aux_qubits = tracker.borrow( + synthesized.num_qubits - len(qubits), context.to_globals(qubits) + ) + global_to_local = context.to_local_mapping() + + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. if synthesized is None and not self._top_level_only: - synthesized = self._unroll_custom_definition(operation, indices) + synthesized = self._get_custom_definition(operation, indices) if synthesized is None: - # if we didn't synthesize, there was nothing to unroll, so just set the used qubits - used_qubits = qubits + # if we didn't synthesize, there was nothing to unroll + # updating the tracker will be handled upstream + pass + + # if it has been synthesized, recurse and finally store the decomposition + elif isinstance(synthesized, Operation): + resynthesized, resynthesized_context = self._synthesize_operation( + synthesized, qubits, tracker, context, use_ancillas=use_ancillas + ) - else: - # if it has been synthesized, recurse and finally store the decomposition - if isinstance(synthesized, Operation): - re_synthesized, qubits = self._synthesize_operation( - synthesized, qubits, tracker.copy() + if resynthesized is not None: + synthesized = resynthesized + else: + tracker.set_dirty(context.to_globals(qubits)) + if isinstance(resynthesized, DAGCircuit): + synthesized_context = resynthesized_context + + elif isinstance(synthesized, QuantumCircuit): + # Synthesized is a quantum circuit which we want to process recursively. + # For example, it's the definition circuit of a custom gate + # or a circuit obtained by calling a synthesis method on a high-level-object. + # In the second case, synthesized may have more qubits than the original node. + + as_dag = circuit_to_dag(synthesized, copy_operations=False) + inner_context = context.restrict(qubits) + + if as_dag.num_qubits() != inner_context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + + # We save the current state of the tracker to be able to return the ancilla + # qubits to the current positions. Note that at this point we do not know + # which ancilla qubits will be allocated. + saved_tracker = tracker.copy() + synthesized = self._run( + as_dag, tracker, inner_context, use_ancillas=use_ancillas, top_level=False + ) + synthesized_context = inner_context + + if (synthesized is not None) and (synthesized.num_qubits() > len(qubits)): + # need to borrow more qubits from tracker + global_aux_qubits = tracker.borrow( + synthesized.num_qubits() - len(qubits), context.to_globals(qubits) + ) + global_to_local = context.to_local_mapping() + + for aq in global_aux_qubits: + if aq in global_to_local: + qubits.append(global_to_local[aq]) + else: + new_local_qubit = context.add_qubit(aq) + qubits.append(new_local_qubit) + + if len(qubits) > num_original_qubits: + tracker.replace_state( + saved_tracker, context.to_globals(qubits[num_original_qubits:]) ) - if re_synthesized is not None: - synthesized = re_synthesized - used_qubits = qubits - - elif isinstance(synthesized, QuantumCircuit): - aux_qubits = tracker.borrow(synthesized.num_qubits - len(qubits), qubits) - used_qubits = qubits + tuple(aux_qubits) - as_dag = circuit_to_dag(synthesized, copy_operations=False) - - # map used qubits to subcircuit - new_qubits = [as_dag.find_bit(q).index for q in as_dag.qubits] - qubit_map = dict(zip(used_qubits, new_qubits)) - - synthesized = self._run(as_dag, tracker.copy(qubit_map)) - if synthesized.num_qubits() != len(used_qubits): - raise RuntimeError( - f"Mismatching number of qubits, using {synthesized.num_qubits()} " - f"but have {len(used_qubits)}." - ) - else: - raise RuntimeError(f"Unexpected synthesized type: {type(synthesized)}") + else: + raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") - if synthesized is not None and used_qubits is None: - raise RuntimeError("Failed to find qubit indices on", synthesized) + if isinstance(synthesized, DAGCircuit) and synthesized_context is None: + raise TranspilerError(f"Synthesized DAGCircuit without data") - return synthesized, used_qubits + return synthesized, synthesized_context - def _unroll_custom_definition( + def _get_custom_definition( self, inst: Instruction, qubits: list[int] | None ) -> QuantumCircuit | None: # check if the operation is already supported natively diff --git a/qiskit/transpiler/passes/synthesis/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py index f3dd34b7df31..e0ae7a08dbd1 100644 --- a/qiskit/transpiler/passes/synthesis/qubit_tracker.py +++ b/qiskit/transpiler/passes/synthesis/qubit_tracker.py @@ -25,108 +25,95 @@ class QubitTracker: unknown state). """ - # This could in future be extended to track different state types, if necessary. - # However, using sets of integers here is much faster than e.g. storing a dictionary with - # {index: state} entries. - qubits: tuple[int] - clean: set[int] - dirty: set[int] - - def num_clean(self, active_qubits: Iterable[int] | None = None): - """Return the number of clean qubits, not considering the active qubits.""" - # this could be cached if getting the set length becomes a performance bottleneck - return len(self.clean.difference(active_qubits or set())) - - def num_dirty(self, active_qubits: Iterable[int] | None = None): - """Return the number of dirty qubits, not considering the active qubits.""" - return len(self.dirty.difference(active_qubits or set())) - - def borrow(self, num_qubits: int, active_qubits: Iterable[int] | None = None) -> list[int]: - """Get ``num_qubits`` qubits, excluding ``active_qubits``.""" - active_qubits = set(active_qubits or []) - available_qubits = [qubit for qubit in self.qubits if qubit not in active_qubits] - - if num_qubits > (available := len(available_qubits)): - raise RuntimeError(f"Cannot borrow {num_qubits} qubits, only {available} available.") - - # for now, prioritize returning clean qubits - available_clean = [qubit for qubit in available_qubits if qubit in self.clean] - available_dirty = [qubit for qubit in available_qubits if qubit in self.dirty] - - borrowed = available_clean[:num_qubits] - return borrowed + available_dirty[: (num_qubits - len(borrowed))] - - def used(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to used (i.e. False).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean -= qubits - self.dirty |= qubits - - def reset(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to 0 (i.e. True).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean |= qubits - self.dirty -= qubits - - def drop(self, qubits: Iterable[int], check: bool = True) -> None: - """Drop qubits from the tracker, meaning that they are no longer available.""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Dropping untracked qubits: {untracked}. Tracker: {self}") - - self.qubits = tuple(qubit for qubit in self.qubits if qubit not in qubits) - self.clean -= qubits - self.dirty -= qubits - - def copy( - self, qubit_map: dict[int, int] | None = None, drop: Iterable[int] | None = None - ) -> "QubitTracker": - """Copy self. - - Args: - qubit_map: If provided, apply the mapping ``{old_qubit: new_qubit}`` to - the qubits in the tracker. Only those old qubits in the mapping will be - part of the new one. - drop: If provided, drop these qubits in the copied tracker. This argument is ignored - if ``qubit_map`` is given, since the qubits can then just be dropped in the map. - """ - if qubit_map is None and drop is not None: - remaining_qubits = [qubit for qubit in self.qubits if qubit not in drop] - qubit_map = dict(zip(remaining_qubits, remaining_qubits)) - - if qubit_map is None: - clean = self.clean.copy() - dirty = self.dirty.copy() - qubits = self.qubits # tuple is immutable, no need to copy - else: - clean, dirty = set(), set() - for old_index, new_index in qubit_map.items(): - if old_index in self.clean: - clean.add(new_index) - elif old_index in self.dirty: - dirty.add(new_index) - else: - raise ValueError(f"Unknown old qubit index: {old_index}. Tracker: {self}") - - qubits = tuple(qubit_map.values()) - - return QubitTracker(qubits, clean=clean, dirty=dirty) + def __init__(self, num_qubits: int): + self.num_qubits = num_qubits + self.state = [False] * num_qubits # True: clean, False: dirty + self.enabled = [True] * num_qubits # True: allowed to use, False: not allowed to use + self.ignored = [False] * num_qubits # Internal scratch space + + def set_dirty(self, qubits): + """Sets state of the given qubits to dirty.""" + for q in qubits: + self.state[q] = False + + def set_clean(self, qubits): + """Sets state of the given qubits to clean.""" + for q in qubits: + self.state[q] = True + + def disable(self, qubits): + """Disables using the given qubits.""" + for q in qubits: + self.enabled[q] = False + + def enable(self, qubits): + """Enables using the given qubits.""" + for q in qubits: + self.enabled[q] = True + + def num_clean(self, ignored_qubits): + """Returns the number of enabled clean qubits, ignoring the given qubits.""" + count = 0 + for q in ignored_qubits: + self.ignored[q] = True + for q in range(self.num_qubits): + if (not self.ignored[q]) and self.enabled[q] and self.state[q]: + count += 1 + for q in ignored_qubits: + self.ignored[q] = False + return count + + def num_dirty(self, ignored_qubits): + """Returns the number of enabled dirty qubits, ignoring the given qubits.""" + count = 0 + for q in ignored_qubits: + self.ignored[q] = True + for q in range(self.num_qubits): + if (not self.ignored[q]) and self.enabled[q] and not self.state[q]: + count += 1 + for q in ignored_qubits: + self.ignored[q] = False + return count + + def borrow(self, num_qubits: int, ignored_qubits: Iterable[int] | None = None) -> list[int]: + """Get ``num_qubits`` enabled qubits, excluding ``ignored_qubits`` and prioritizing + clean qubits.""" + res = [] + for q in ignored_qubits: + self.ignored[q] = True + for q in range(self.num_qubits): + if (not self.ignored[q]) and self.enabled[q] and self.state[q]: + res.append(q) + for q in range(self.num_qubits): + if (not self.ignored[q]) and self.enabled[q] and not self.state[q]: + res.append(q) + for q in ignored_qubits: + self.ignored[q] = False + return res[:num_qubits] + + def copy(self) -> "QubitTracker": + """Copies the qubit tracker.""" + tracker = QubitTracker(self.num_qubits) + tracker.state = self.state.copy() + tracker.enabled = self.enabled.copy() + # no need to copy the scratch space (ignored) + return tracker + + def replace_state(self, other: "QubitTracker", qubits): + """Replaces the state of the given qubits by their state in the ``other`` tracker.""" + for q in qubits: + self.state[q] = other.state[q] def __str__(self) -> str: - return ( - f"QubitTracker({len(self.qubits)}, clean: {self.num_clean()}, dirty: {self.num_dirty()})" - + f"\n\tclean: {self.clean}" - + f"\n\tdirty: {self.dirty}" - ) + """Pretty-prints qubit states.""" + s = "" + for q in range(self.num_qubits): + s += str(q) + ": " + if not self.enabled[q]: + s += "_" + elif self.state[q]: + s += "0" + else: + s += "*" + s += "; " + return s diff --git a/releasenotes/notes/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml b/releasenotes/notes/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml new file mode 100644 index 000000000000..f832fed1b72d --- /dev/null +++ b/releasenotes/notes/improve-hls-qubit-tracking-6b6288d556e3af9d.yaml @@ -0,0 +1,8 @@ +--- +features_transpiler: + - | + Improved handling of ancilla qubits in the :class:`.HighLevelSynthesis` + transpiler pass. For example, a circuit may have custom gates whose + definitions include :class:`.MCXGate`\s. Now the synthesis algorithms + for the inner MCX-gates can use the ancilla qubits available on the + global circuit but outside the custom gates' definitions. diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index a7dd806e63e9..73061854b689 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -45,6 +45,7 @@ QFTGate, IGate, MCXGate, + SGate, ) from qiskit.circuit.library.generalized_gates import LinearFunction from qiskit.quantum_info import Clifford, Operator, Statevector @@ -1489,6 +1490,137 @@ def test_transpile_power_high_level_object(self): for op in ops: self.assertIn(op, ["u", "cx", "ecr", "measure"]) + def test_simple_circuit(self): + """Test HLS on a simple circuit.""" + qc = QuantumCircuit(3) + qc.cz(1, 2) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_simple_circuit2(self): + """Test HLS on a simple circuit.""" + qc = QuantumCircuit(6) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 3) + qc.h(5) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u", "h"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def(self): + """Test recursive synthesis of the definition circuit.""" + inner = QuantumCircuit(2) + inner.cz(0, 1) + qc = QuantumCircuit(3) + qc.append(inner.to_gate(), [0, 2]) + pass_ = HighLevelSynthesis(basis_gates=["cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def2(self): + """Test recursive synthesis of the definition circuit.""" + inner1 = QuantumCircuit(2) + inner1.cz(0, 1) + qc = QuantumCircuit(4) + qc.append(inner1.to_instruction(), [2, 3]) + pass_ = HighLevelSynthesis(basis_gates=["cz", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_recursive_def3(self): + """Test recursive synthesis of the definition circuit.""" + inner2 = QuantumCircuit(2) + inner2.h(0) + inner2.cx(0, 1) + + inner1 = QuantumCircuit(4) + inner1.cz(0, 1) + inner1.append(inner2.to_instruction(), [0, 2]) + + qc = QuantumCircuit(6) + qc.h(1) + qc.h(2) + qc.cz(1, 2) + qc.append(inner1.to_instruction(), [2, 0, 4, 3]) + qc.h(2) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_circuit_with_mcx(self): + """Test synthesis with plugins.""" + qc = QuantumCircuit(10) + qc.mcx([3, 4, 5, 6, 7], 2) + basis_gates = ["u", "cx"] + qct = HighLevelSynthesis(basis_gates=basis_gates)(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_circuit_with_mcx_def(self): + """Test synthesis where the plugin is called within the recursive call + on the definition.""" + circuit = QuantumCircuit(6) + circuit.mcx([0, 1, 2, 3, 4], 5) + custom_gate = circuit.to_gate() + qc = QuantumCircuit(10) + qc.append(custom_gate, [3, 4, 5, 6, 7, 2]) + basis_gates = ["u", "cx"] + qct = HighLevelSynthesis(basis_gates=basis_gates)(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_circuit_with_mcx_def_rec(self): + """Test synthesis where the plugin is called within the recursive call + on the definition.""" + inner2 = QuantumCircuit(6) + inner2.mcx([0, 1, 2, 3, 4], 5) + inner1 = QuantumCircuit(7) + inner1.append(inner2.to_gate(), [1, 2, 3, 4, 5, 6]) + qc = QuantumCircuit(10) + qc.append(inner1.to_gate(), [2, 3, 4, 5, 6, 7, 8]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_annotated_gate(self): + """Test synthesis with annotated gate.""" + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(SGate().control(3, annotated=True), [0, 1, 8, 9]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Operator(qc), Operator(qct)) + + def test_annotated_circuit(self): + """Test synthesis with annotated custom gate.""" + circ = QuantumCircuit(2) + circ.h(0) + circ.cy(0, 1) + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(circ.to_gate().control(3, annotated=True), [2, 0, 3, 7, 8]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + + def test_annotated_rec(self): + """Test synthesis with annotated custom gates and recursion.""" + inner2 = QuantumCircuit(2) + inner2.h(0) + inner2.cy(0, 1) + inner1 = QuantumCircuit(5) + inner1.h(1) + inner1.append(inner2.to_gate().control(2, annotated=True), [1, 2, 3, 4]) + qc = QuantumCircuit(10) + qc.x(1) + qc.cz(1, 2) + qc.append(inner1.to_gate().control(3, annotated=True), [9, 8, 7, 6, 5, 4, 3, 2]) + pass_ = HighLevelSynthesis(basis_gates=["h", "z", "cx", "u"]) + qct = pass_(qc) + self.assertEqual(Statevector(qc), Statevector(qct)) + class TestUnrollerCompatability(QiskitTestCase): """Tests backward compatibility with the UnrollCustomDefinitions pass. From ec3d7659bf763d43d1f9962b16f45b842fa44e6b Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Mon, 30 Sep 2024 12:39:20 +0300 Subject: [PATCH 2/8] lint --- .../passes/synthesis/high_level_synthesis.py | 4 ++-- .../transpiler/passes/synthesis/qubit_tracker.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 171e6f83ca63..7778628770ab 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -549,7 +549,7 @@ def _synthesize_operation( # Do not allow access to control qubits tracker.disable(context.to_globals(control_qubits)) - synthesized_base_op, baseop_data = self._synthesize_operation( + synthesized_base_op, _ = self._synthesize_operation( operation.base_op, baseop_qubits, tracker, @@ -683,7 +683,7 @@ def _synthesize_operation( raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") if isinstance(synthesized, DAGCircuit) and synthesized_context is None: - raise TranspilerError(f"Synthesized DAGCircuit without data") + raise TranspilerError("HighLevelSynthesis internal error.") return synthesized, synthesized_context diff --git a/qiskit/transpiler/passes/synthesis/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py index e0ae7a08dbd1..9db4f6141d90 100644 --- a/qiskit/transpiler/passes/synthesis/qubit_tracker.py +++ b/qiskit/transpiler/passes/synthesis/qubit_tracker.py @@ -106,14 +106,14 @@ def replace_state(self, other: "QubitTracker", qubits): def __str__(self) -> str: """Pretty-prints qubit states.""" - s = "" + out = "" for q in range(self.num_qubits): - s += str(q) + ": " + out += str(q) + ": " if not self.enabled[q]: - s += "_" + out += "_" elif self.state[q]: - s += "0" + out += "0" else: - s += "*" - s += "; " - return s + out += "*" + out += "; " + return out From 112e9e3d183b23bab83960e0050e94054b36911d Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 10:59:16 +0300 Subject: [PATCH 3/8] changing the internals of QubitContext to be a list rathen than dict --- .../passes/synthesis/high_level_synthesis.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 1f86911ed51a..aeec06d392c5 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -139,16 +139,18 @@ class QubitContext: """Correspondence between local qubits and global qubits. An internal class for handling recursion within HighLevelSynthesis. - Provides the correspondence between the qubit indices of an internal DAG, + Provides correspondence between the qubit indices of an internal DAG, aka the "local qubits" (for instance, of the definition circuit of a custom gate), and the qubit indices of the original DAG, aka the "global qubits". - The local qubit indices are assumed to be consecutive integers starting at - zero, i.e. 0, 1, 2, etc. + Since the local qubits are consecutive integers starting at zero, + i.e. 0, 1, 2, etc., the correspondence is kept using a list, with the + entry in position `k` representing the global qubit that corresponds + to the local qubit `k`. """ - def __init__(self, local_to_global: dict): + def __init__(self, local_to_global: list): self.local_to_global = local_to_global def num_qubits(self) -> int: @@ -161,22 +163,22 @@ def add_qubit(self, global_qubit) -> int: new local qubit. """ new_local_qubit = len(self.local_to_global) - self.local_to_global[new_local_qubit] = global_qubit + self.local_to_global.append(global_qubit) return new_local_qubit - def to_global_mapping(self): + def to_global_mapping(self) -> list: """Returns the local-to-global mapping.""" return self.local_to_global - def to_local_mapping(self): - """Returns the global-to-local mapping.""" - return {j: i for (i, j) in self.local_to_global.items()} + def to_local_mapping(self) -> dict: + """Returns the global-to-local mapping .""" + return {j: i for (i, j) in enumerate(self.local_to_global)} def restrict(self, qubits: list[int] | tuple[int]) -> "QubitContext": - """Restricts the context to a subset of qubits, remapping the indices as + """Restricts the context to a subset of qubits, remapping the indices to be consecutive integers starting at zero. """ - return QubitContext({i: self.local_to_global[qubits[i]] for i in range(len(qubits))}) + return QubitContext([self.local_to_global[q] for q in qubits]) def to_global(self, qubit: int) -> int: """Returns the global qubits corresponding to the given local qubits.""" @@ -323,7 +325,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ qubits = tuple(dag.find_bit(q).index for q in dag.qubits) - context = QubitContext({i: i for i in range(len(dag.qubits))}) + context = QubitContext([i for i in range(len(dag.qubits))]) tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.qubits_initially_zero: tracker.set_clean(context.to_globals(qubits)) From 7432b6b78f8b10c9cb3443478174ade6b7fb2be9 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 11:43:10 +0300 Subject: [PATCH 4/8] temporarily disabling a failing test --- test/python/circuit/library/test_mcmt.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index ead6a07d8b4d..e130d8e09d29 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -182,15 +182,19 @@ def test_default_plugin(self): hls = HighLevelSynthesis() - # test a decomposition without sufficient ancillas for MCMT V-chain - with self.subTest(msg="insufficient auxiliaries"): - circuit = QuantumCircuit(num_controls + num_target + num_vchain_ancillas - 1) - circuit.append(mcmt, range(mcmt.num_qubits)) - - synthesized = hls(circuit) - num_idle = len(list(circuit_to_dag(synthesized).idle_wires())) - - self.assertEqual(num_idle, num_vchain_ancillas - 1) + # Note: I am temporarily disabling this test: I need to debug whether it's a + # spurious failure (coming from the increased power of HLS to exploit + # ancilla qubits) or a real problem. + + # # test a decomposition without sufficient ancillas for MCMT V-chain + # with self.subTest(msg="insufficient auxiliaries"): + # circuit = QuantumCircuit(num_controls + num_target + num_vchain_ancillas - 1) + # circuit.append(mcmt, range(mcmt.num_qubits)) + # + # synthesized = hls(circuit) + # num_idle = len(list(circuit_to_dag(synthesized).idle_wires())) + # + # self.assertEqual(num_idle, num_vchain_ancillas - 1) # test with enough auxiliary qubits available with self.subTest(msg="enough auxiliaries"): From 02ca8aa1b0f4828e6cb4cec16c4cabc9af588711 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 12:00:15 +0300 Subject: [PATCH 5/8] renaming to _local_to_global following review comments --- .../passes/synthesis/high_level_synthesis.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index aeec06d392c5..d8693ff0f232 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -151,42 +151,42 @@ class QubitContext: """ def __init__(self, local_to_global: list): - self.local_to_global = local_to_global + self._local_to_global = local_to_global def num_qubits(self) -> int: """Returns the number of local qubits.""" - return len(self.local_to_global) + return len(self._local_to_global) def add_qubit(self, global_qubit) -> int: """Extends the correspondence by an additional qubit that maps to the given global qubit. Returns the index of the new local qubit. """ - new_local_qubit = len(self.local_to_global) - self.local_to_global.append(global_qubit) + new_local_qubit = len(self._local_to_global) + self._local_to_global.append(global_qubit) return new_local_qubit def to_global_mapping(self) -> list: """Returns the local-to-global mapping.""" - return self.local_to_global + return self._local_to_global def to_local_mapping(self) -> dict: """Returns the global-to-local mapping .""" - return {j: i for (i, j) in enumerate(self.local_to_global)} + return {j: i for (i, j) in enumerate(self._local_to_global)} def restrict(self, qubits: list[int] | tuple[int]) -> "QubitContext": """Restricts the context to a subset of qubits, remapping the indices to be consecutive integers starting at zero. """ - return QubitContext([self.local_to_global[q] for q in qubits]) + return QubitContext([self._local_to_global[q] for q in qubits]) def to_global(self, qubit: int) -> int: """Returns the global qubits corresponding to the given local qubits.""" - return self.local_to_global[qubit] + return self._local_to_global[qubit] def to_globals(self, qubits: list[int]) -> list[int]: """Returns the global qubits corresponding to the given local qubits.""" - return [self.local_to_global[q] for q in qubits] + return [self._local_to_global[q] for q in qubits] class HighLevelSynthesis(TransformationPass): From 60e745607d27f01a16d9c8f80061bdb91ad7ccb1 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 12:44:36 +0300 Subject: [PATCH 6/8] pretty print --- qiskit/transpiler/passes/synthesis/qubit_tracker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py index 9db4f6141d90..162d28bf8e9e 100644 --- a/qiskit/transpiler/passes/synthesis/qubit_tracker.py +++ b/qiskit/transpiler/passes/synthesis/qubit_tracker.py @@ -106,7 +106,7 @@ def replace_state(self, other: "QubitTracker", qubits): def __str__(self) -> str: """Pretty-prints qubit states.""" - out = "" + out = "QubitTracker(" for q in range(self.num_qubits): out += str(q) + ": " if not self.enabled[q]: @@ -115,5 +115,8 @@ def __str__(self) -> str: out += "0" else: out += "*" - out += "; " + if q != self.num_qubits - 1: + out += "; " + else: + out += ")" return out From 1e10c0d0110c005f34b7b4c348af5a3cf1181612 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 13:46:18 +0300 Subject: [PATCH 7/8] fixing test --- test/python/circuit/library/test_mcmt.py | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index e130d8e09d29..73befb19db46 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -180,21 +180,19 @@ def test_default_plugin(self): gate = XGate() mcmt = MCMTGate(gate, num_controls, num_target) - hls = HighLevelSynthesis() - - # Note: I am temporarily disabling this test: I need to debug whether it's a - # spurious failure (coming from the increased power of HLS to exploit - # ancilla qubits) or a real problem. - - # # test a decomposition without sufficient ancillas for MCMT V-chain - # with self.subTest(msg="insufficient auxiliaries"): - # circuit = QuantumCircuit(num_controls + num_target + num_vchain_ancillas - 1) - # circuit.append(mcmt, range(mcmt.num_qubits)) - # - # synthesized = hls(circuit) - # num_idle = len(list(circuit_to_dag(synthesized).idle_wires())) - # - # self.assertEqual(num_idle, num_vchain_ancillas - 1) + # make sure MCX-synthesis does not use ancilla qubits + config = HLSConfig(mcx=["noaux_v24"]) + hls = HighLevelSynthesis(hls_config=config) + + # test a decomposition without sufficient ancillas for MCMT V-chain + with self.subTest(msg="insufficient auxiliaries"): + circuit = QuantumCircuit(num_controls + num_target + num_vchain_ancillas - 1) + circuit.append(mcmt, range(mcmt.num_qubits)) + + synthesized = hls(circuit) + num_idle = len(list(circuit_to_dag(synthesized).idle_wires())) + + self.assertEqual(num_idle, num_vchain_ancillas - 1) # test with enough auxiliary qubits available with self.subTest(msg="enough auxiliaries"): From 175fbaf0c3bbc369c783404e76d7430b91d4612d Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 24 Oct 2024 16:56:07 +0300 Subject: [PATCH 8/8] lint --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index d8693ff0f232..870217f64754 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -325,7 +325,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ qubits = tuple(dag.find_bit(q).index for q in dag.qubits) - context = QubitContext([i for i in range(len(dag.qubits))]) + context = QubitContext(list(range(len(dag.qubits)))) tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.qubits_initially_zero: tracker.set_clean(context.to_globals(qubits))