From d0b4137b0a51068b382a961943b222909d6280cc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 11 Dec 2024 15:40:27 +0200 Subject: [PATCH 01/32] Moving all internal fields into a separate class --- .../passes/synthesis/high_level_synthesis.py | 112 ++++++++++++------ 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 95adee1d05c7..26add7b08672 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -135,6 +135,34 @@ def set_methods(self, hls_name, hls_methods): self.methods[hls_name] = hls_methods +class HLSData: + """Internal class for keeping immutable data required by HighLevelSynthesis.""" + + def __init__( + self, + hls_config, + hls_plugin_manager, + coupling_map, + target, + use_qubit_indices, + qubits_initially_zero, + equivalence_library, + min_qubits, + top_level_only, + device_insts, + ): + self.hls_config = hls_config + self.hls_plugin_manager = hls_plugin_manager + self.coupling_map = coupling_map + self.target = target + self.use_qubit_indices = use_qubit_indices + self.qubits_initially_zero = qubits_initially_zero + self.equivalence_library = equivalence_library + self.min_qubits = min_qubits + self.top_level_only = top_level_only + self.device_insts = device_insts + + class HighLevelSynthesis(TransformationPass): r"""Synthesize higher-level objects and unroll custom definitions. @@ -230,32 +258,37 @@ def __init__( """ super().__init__() - if hls_config is not None: - self.hls_config = hls_config - else: - # When the config file is not provided, we will use the "default" method - # to synthesize Operations (when available). - self.hls_config = HLSConfig(True) - - self.hls_plugin_manager = HighLevelSynthesisPluginManager() - self._coupling_map = coupling_map - self._target = target - self._use_qubit_indices = use_qubit_indices - self.qubits_initially_zero = qubits_initially_zero + # When the config file is not provided, we will use the "default" method + # to synthesize Operations (when available). + hls_config = hls_config or HLSConfig(True) + hls_plugin_manager = HighLevelSynthesisPluginManager() + if target is not None: - self._coupling_map = self._target.build_coupling_map() - self._equiv_lib = equivalence_library - self._basis_gates = basis_gates - self._min_qubits = min_qubits + coupling_map = target.build_coupling_map() + else: + coupling_map = coupling_map - self._top_level_only = self._basis_gates is None and self._target is None + top_level_only = basis_gates is None and target is None # include path for when target exists but target.num_qubits is None (BasicSimulator) - if not self._top_level_only and (self._target is None or self._target.num_qubits is None): + if not top_level_only and (target is None or target.num_qubits is None): basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} - self._device_insts = basic_insts | set(self._basis_gates) + device_insts = basic_insts | set(basis_gates) else: - self._device_insts = set() + device_insts = set() + + self.data = HLSData( + hls_config=hls_config, + hls_plugin_manager=hls_plugin_manager, + coupling_map=coupling_map, + target=target, + use_qubit_indices=use_qubit_indices, + qubits_initially_zero=qubits_initially_zero, + equivalence_library=equivalence_library, + min_qubits=min_qubits, + top_level_only=top_level_only, + device_insts=device_insts, + ) def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the HighLevelSynthesis pass on `dag`. @@ -273,7 +306,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: qubits = tuple(dag.find_bit(q).index for q in dag.qubits) context = QubitContext(list(range(len(dag.qubits)))) tracker = QubitTracker(num_qubits=dag.num_qubits()) - if self.qubits_initially_zero: + if self.data.qubits_initially_zero: tracker.set_clean(context.to_globals(qubits)) out_dag = self._run(dag, tracker, context, use_ancillas=True, top_level=True) @@ -535,7 +568,7 @@ def _synthesize_operation( # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. else: # Try synthesis via HLS -- which will return ``None`` if unsuccessful. - indices = qubits if self._use_qubit_indices else None + indices = qubits if self.data.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)) @@ -567,7 +600,7 @@ def _synthesize_operation( 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: + if synthesized is None and not self.data.top_level_only: synthesized = self._get_custom_definition(operation, indices) if synthesized is None: @@ -643,7 +676,10 @@ def _get_custom_definition( if not (isinstance(inst, ControlledGate) and inst._open_ctrl): # include path for when target exists but target.num_qubits is None (BasicSimulator) inst_supported = self._instruction_supported(inst.name, qubits) - if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(inst)): + if inst_supported or ( + self.data.equivalence_library is not None + and self.data.equivalence_library.has_entry(inst) + ): return None # we support this operation already # if not, try to get the definition @@ -659,13 +695,13 @@ def _get_custom_definition( def _methods_to_try(self, name: str): """Get a sequence of methods to try for a given op name.""" - if (methods := self.hls_config.methods.get(name)) is not None: + if (methods := self.data.hls_config.methods.get(name)) is not None: # the operation's name appears in the user-provided config, # we use the list of methods provided by the user return methods if ( - self.hls_config.use_default_on_unspecified - and "default" in self.hls_plugin_manager.method_names(name) + self.data.hls_config.use_default_on_unspecified + and "default" in self.data.hls_plugin_manager.method_names(name) ): # the operation's name does not appear in the user-specified config, # we use the "default" method when instructed to do so and the "default" @@ -692,7 +728,7 @@ def _synthesize_op_using_plugins( when no synthesis methods is available or specified, or when there is an insufficient number of auxiliary qubits). """ - hls_plugin_manager = self.hls_plugin_manager + hls_plugin_manager = self.data.hls_plugin_manager best_decomposition = None best_score = np.inf @@ -732,8 +768,8 @@ def _synthesize_op_using_plugins( decomposition = plugin_method.run( op, - coupling_map=self._coupling_map, - target=self._target, + coupling_map=self.data.coupling_map, + target=self.data.target, qubits=qubits, **plugin_args, ) @@ -741,7 +777,7 @@ def _synthesize_op_using_plugins( # The synthesis methods that are not suited for the given higher-level-object # will return None. if decomposition is not None: - if self.hls_config.plugin_selection == "sequential": + if self.data.hls_config.plugin_selection == "sequential": # In the "sequential" mode the first successful decomposition is # returned. best_decomposition = decomposition @@ -749,7 +785,7 @@ def _synthesize_op_using_plugins( # In the "run everything" mode we update the best decomposition # discovered - current_score = self.hls_config.plugin_evaluation_fn(decomposition) + current_score = self.data.hls_config.plugin_evaluation_fn(decomposition) if current_score < best_score: best_decomposition = decomposition best_score = current_score @@ -812,7 +848,7 @@ def _definitely_skip_node( if ( dag._has_calibration_for(node) - or len(node.qargs) < self._min_qubits + or len(node.qargs) < self.data.min_qubits or node.is_directive() or (self._instruction_supported(node.name, qubits) and not node.is_control_flow()) ): @@ -835,17 +871,17 @@ def _definitely_skip_node( # This uses unfortunately private details of `EquivalenceLibrary`, but so does the # `BasisTranslator`, and this is supposed to just be temporary til this is moved # into Rust space. - self._equiv_lib is not None + self.data.equivalence_library is not None and equivalence.Key(name=node.name, num_qubits=node.num_qubits) - in self._equiv_lib.keys() + in self.data.equivalence_library.keys() ) ) def _instruction_supported(self, name: str, qubits: tuple[int] | None) -> bool: # include path for when target exists but target.num_qubits is None (BasicSimulator) - if self._target is None or self._target.num_qubits is None: - return name in self._device_insts - return self._target.instruction_supported(operation_name=name, qargs=qubits) + if self.data.target is None or self.data.target.num_qubits is None: + return name in self.data.device_insts + return self.data.target.instruction_supported(operation_name=name, qargs=qubits) def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: From 1f5a69410036fcfedc799d5f492bb4d2640944b4 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 11 Dec 2024 18:30:11 +0200 Subject: [PATCH 02/32] making all the inner functions to be global --- .../passes/synthesis/high_level_synthesis.py | 1004 +++++++++-------- 1 file changed, 505 insertions(+), 499 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 26add7b08672..99533c4b4e86 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -309,343 +309,289 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.data.qubits_initially_zero: tracker.set_clean(context.to_globals(qubits)) - out_dag = self._run(dag, tracker, context, use_ancillas=True, top_level=True) + out_dag = _run(dag, self.data, tracker, context, use_ancillas=True, top_level=True) return out_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. - - 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.") +def _run( + dag: DAGCircuit, + data: HLSData, + tracker: QubitTracker, + context: QubitContext, + use_ancillas: bool, + top_level: bool, +) -> DAGCircuit: + """ + The main recursive function that synthesizes a DAGCircuit. - # 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 - 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 - elif self._definitely_skip_node(node, qubits, dag): - tracker.set_dirty(context.to_globals(qubits)) - - # next check control flow - elif node.is_control_flow(): - 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, - ) + 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. - # now we are free to synthesize - else: - # 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 - ) + The function returns the synthesized DAG. - # If the synthesis changed the operation (i.e. it is not None), store the result. - if synthesized is not None: - synthesized_nodes[node._node_id] = (synthesized, synthesized_context) + 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 the synthesis did not change anything, just update the qubit tracker. - elif not processed: - tracker.set_dirty(context.to_globals(qubits)) + if dag.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") - # We did not change anything just return the input. - if len(synthesized_nodes) == 0: + # 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 _definitely_skip_node(data, 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 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() - 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() + # 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 + synthesized_context = None - for node in dag.topological_op_nodes(): + # 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 - if op_tuple := synthesized_nodes.get(node._node_id, None): - op, op_context = op_tuple + elif node.op.name == "reset": + # reset qubits to 0 + tracker.set_clean(context.to_globals(qubits)) + processed = True - if isinstance(op, Operation): - out.apply_operation_back(op, node.qargs, node.cargs) - continue + # check if synthesis for the operation can be skipped + elif _definitely_skip_node(data, node, qubits, dag): + tracker.set_dirty(context.to_globals(qubits)) - if isinstance(op, QuantumCircuit): - op = circuit_to_dag(op, copy_operations=False) + # next check control flow + elif node.is_control_flow(): + inner_context = context.restrict(qubits) + synthesized = control_flow.map_blocks( + partial( + _run, + data=data, + tracker=tracker, + context=inner_context, + use_ancillas=False, + top_level=False, + ), + node.op, + ) - inner_to_global = op_context.to_global_mapping() - if isinstance(op, DAGCircuit): - qubit_map = { - 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)) + # now we are free to synthesize + else: + # 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 = _synthesize_operation( + data, node.op, qubits, tracker, context, use_ancillas=use_ancillas + ) - for sub_node in op.op_nodes(): - out.apply_operation_back( - sub_node.op, - tuple(qubit_map[qarg] for qarg in sub_node.qargs), - tuple(clbit_map[carg] for carg in sub_node.cargs), - ) - out.global_phase += op.global_phase + # If the synthesis changed the operation (i.e. it is not None), store the result. + if synthesized is not None: + synthesized_nodes[node._node_id] = (synthesized, synthesized_context) - else: - raise TranspilerError(f"Unexpected synthesized type: {type(op)}") - else: - out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + # If the synthesis did not change anything, just update the qubit tracker. + elif not processed: + tracker.set_dirty(context.to_globals(qubits)) - if out.num_qubits() != context.num_qubits(): + # 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 - return out + # 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() + num_additional_qubits = context.num_qubits() - out.num_qubits() - def _synthesize_operation( - self, - operation: Operation, - qubits: tuple[int], - tracker: QubitTracker, - 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). - """ + if num_additional_qubits > 0: + out.add_qubits([Qubit() for _ in range(num_additional_qubits)]) - synthesized_context = None + index_to_qubit = dict(enumerate(out.qubits)) + outer_to_local = context.to_local_mapping() - # 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) - # or operation (e.g adding control on an X gate). - # (2) High-level objects: try running the battery of high-level synthesis plugins (e.g. - # 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) + for node in dag.topological_op_nodes(): - synthesized = None + if op_tuple := synthesized_nodes.get(node._node_id, None): + op, op_context = op_tuple - # 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: - # 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 - - # get qubits of base operation - control_qubits = qubits[0:num_ctrl] - - # Do not allow access to control qubits - tracker.disable(context.to_globals(control_qubits)) - synthesized_base_op, _ = self._synthesize_operation( - operation.base_op, - baseop_qubits, - tracker, - context, - use_ancillas=use_ancillas, - ) + if isinstance(op, Operation): + out.apply_operation_back(op, node.qargs, node.cargs) + continue - 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) + if isinstance(op, QuantumCircuit): + op = circuit_to_dag(op, copy_operations=False) - # 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)) + inner_to_global = op_context.to_global_mapping() + if isinstance(op, DAGCircuit): + qubit_map = { + 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)) - # This step currently does not introduce ancilla qubits. - synthesized = self._apply_annotations(synthesized_base_op, operation.modifiers) + for sub_node in op.op_nodes(): + out.apply_operation_back( + sub_node.op, + tuple(qubit_map[qarg] for qarg in sub_node.qargs), + tuple(clbit_map[carg] for carg in sub_node.cargs), + ) + out.global_phase += op.global_phase - # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. + else: + raise TranspilerError(f"Unexpected synthesized type: {type(op)}") else: - # Try synthesis via HLS -- which will return ``None`` if unsuccessful. - indices = qubits if self.data.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, - num_clean_available, - num_dirty_available, - ) + out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - # 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.data.top_level_only: - synthesized = self._get_custom_definition(operation, indices) - - if synthesized is None: - # 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 - ) + if out.num_qubits() != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") - if resynthesized is not None: - synthesized = resynthesized - else: - tracker.set_dirty(context.to_globals(qubits)) - if isinstance(resynthesized, DAGCircuit): - synthesized_context = resynthesized_context + return out - 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) +def _synthesize_operation( + data: HLSData, + operation: Operation, + qubits: tuple[int], + tracker: QubitTracker, + 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). + """ - if as_dag.num_qubits() != inner_context.num_qubits(): - raise TranspilerError("HighLevelSynthesis internal error.") + 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) + # or operation (e.g adding control on an X gate). + # (2) High-level objects: try running the battery of high-level synthesis plugins (e.g. + # 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: + # 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 + + # get qubits of base operation + control_qubits = qubits[0:num_ctrl] + + # Do not allow access to control qubits + tracker.disable(context.to_globals(control_qubits)) + synthesized_base_op, _ = _synthesize_operation( + data, + operation.base_op, + baseop_qubits, + tracker, + context, + use_ancillas=use_ancillas, + ) - # 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 + 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 = _apply_annotations(data, synthesized_base_op, operation.modifiers) + + # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. + else: + # Try synthesis via HLS -- which will return ``None`` if unsuccessful. + indices = qubits if data.use_qubit_indices else None + if len(hls_methods := _methods_to_try(data, 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 = _synthesize_op_using_plugins( + data, + hls_methods, + operation, + indices, + num_clean_available, + num_dirty_available, ) - synthesized_context = inner_context - if (synthesized is not None) and (synthesized.num_qubits() > len(qubits)): + # 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) + synthesized.num_qubits - len(qubits), context.to_globals(qubits) ) global_to_local = context.to_local_mapping() @@ -656,232 +602,292 @@ def _synthesize_operation( 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 HLS did not apply, or was unsuccessful, try unrolling custom definitions. + if synthesized is None and not data.top_level_only: + synthesized = _get_custom_definition(data, operation, indices) + + if synthesized is None: + # 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 = _synthesize_operation( + data, synthesized, qubits, tracker, context, use_ancillas=use_ancillas + ) + if resynthesized is not None: + synthesized = resynthesized else: - raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") + 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. - if isinstance(synthesized, DAGCircuit) and synthesized_context is None: + 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.") - return synthesized, synthesized_context - - def _get_custom_definition( - self, inst: Instruction, qubits: list[int] | None - ) -> QuantumCircuit | None: - # check if the operation is already supported natively - if not (isinstance(inst, ControlledGate) and inst._open_ctrl): - # include path for when target exists but target.num_qubits is None (BasicSimulator) - inst_supported = self._instruction_supported(inst.name, qubits) - if inst_supported or ( - self.data.equivalence_library is not None - and self.data.equivalence_library.has_entry(inst) - ): - return None # we support this operation already - - # if not, try to get the definition - try: - definition = inst.definition - except (TypeError, AttributeError) as err: - raise TranspilerError(f"HighLevelSynthesis was unable to define {inst.name}.") from err - - if definition is None: - raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {inst}.") - - return definition - - def _methods_to_try(self, name: str): - """Get a sequence of methods to try for a given op name.""" - if (methods := self.data.hls_config.methods.get(name)) is not None: - # the operation's name appears in the user-provided config, - # we use the list of methods provided by the user - return methods - if ( - self.data.hls_config.use_default_on_unspecified - and "default" in self.data.hls_plugin_manager.method_names(name) + # 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 = _run( + as_dag, data, 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:])) + + else: + raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") + + if isinstance(synthesized, DAGCircuit) and synthesized_context is None: + raise TranspilerError("HighLevelSynthesis internal error.") + + return synthesized, synthesized_context + + +def _get_custom_definition( + data: HLSData, inst: Instruction, qubits: list[int] | None +) -> QuantumCircuit | None: + # check if the operation is already supported natively + if not (isinstance(inst, ControlledGate) and inst._open_ctrl): + # include path for when target exists but target.num_qubits is None (BasicSimulator) + inst_supported = _instruction_supported(data, inst.name, qubits) + if inst_supported or ( + data.equivalence_library is not None and data.equivalence_library.has_entry(inst) ): - # the operation's name does not appear in the user-specified config, - # we use the "default" method when instructed to do so and the "default" - # method is available - return ["default"] - return [] + return None # we support this operation already - def _synthesize_op_using_plugins( - self, - hls_methods: list, - op: Operation, - qubits: list[int] | None, - num_clean_ancillas: int = 0, - num_dirty_ancillas: int = 0, - ) -> QuantumCircuit | None: - """ - Attempts to synthesize op using plugin mechanism. + # if not, try to get the definition + try: + definition = inst.definition + except (TypeError, AttributeError) as err: + raise TranspilerError(f"HighLevelSynthesis was unable to define {inst.name}.") from err - The arguments ``num_clean_ancillas`` and ``num_dirty_ancillas`` specify - the number of clean and dirty qubits available to synthesize the given - operation. A synthesis method does not need to use these additional qubits. + if definition is None: + raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {inst}.") - Returns either the synthesized circuit or None (which may occur - when no synthesis methods is available or specified, or when there is - an insufficient number of auxiliary qubits). - """ - hls_plugin_manager = self.data.hls_plugin_manager - - best_decomposition = None - best_score = np.inf - - for method in hls_methods: - # There are two ways to specify a synthesis method. The more explicit - # way is to specify it as a tuple consisting of a synthesis algorithm and a - # list of additional arguments, e.g., - # ("kms", {"all_mats": 1, "max_paths": 100, "orig_circuit": 0}), or - # ("pmh", {}). - # When the list of additional arguments is empty, one can also specify - # just the synthesis algorithm, e.g., - # "pmh". - if isinstance(method, tuple): - plugin_specifier, plugin_args = method - else: - plugin_specifier = method - plugin_args = {} - - # There are two ways to specify a synthesis algorithm being run, - # either by name, e.g. "kms" (which then should be specified in entry_points), - # or directly as a class inherited from HighLevelSynthesisPlugin (which then - # does not need to be specified in entry_points). - if isinstance(plugin_specifier, str): - if plugin_specifier not in hls_plugin_manager.method_names(op.name): - raise TranspilerError( - f"Specified method: {plugin_specifier} not found in available " - f"plugins for {op.name}" - ) - plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) - else: - plugin_method = plugin_specifier - - # Set the number of available clean and dirty auxiliary qubits via plugin args. - plugin_args["num_clean_ancillas"] = num_clean_ancillas - plugin_args["num_dirty_ancillas"] = num_dirty_ancillas - - decomposition = plugin_method.run( - op, - coupling_map=self.data.coupling_map, - target=self.data.target, - qubits=qubits, - **plugin_args, + return definition + + +def _methods_to_try(data: HLSData, name: str): + """Get a sequence of methods to try for a given op name.""" + if (methods := data.hls_config.methods.get(name)) is not None: + # the operation's name appears in the user-provided config, + # we use the list of methods provided by the user + return methods + if ( + data.hls_config.use_default_on_unspecified + and "default" in data.hls_plugin_manager.method_names(name) + ): + # the operation's name does not appear in the user-specified config, + # we use the "default" method when instructed to do so and the "default" + # method is available + return ["default"] + return [] + + +def _synthesize_op_using_plugins( + data: HLSData, + hls_methods: list, + op: Operation, + qubits: list[int] | None, + num_clean_ancillas: int = 0, + num_dirty_ancillas: int = 0, +) -> QuantumCircuit | None: + """ + Attempts to synthesize op using plugin mechanism. + + The arguments ``num_clean_ancillas`` and ``num_dirty_ancillas`` specify + the number of clean and dirty qubits available to synthesize the given + operation. A synthesis method does not need to use these additional qubits. + + Returns either the synthesized circuit or None (which may occur + when no synthesis methods is available or specified, or when there is + an insufficient number of auxiliary qubits). + """ + hls_plugin_manager = data.hls_plugin_manager + + best_decomposition = None + best_score = np.inf + + for method in hls_methods: + # There are two ways to specify a synthesis method. The more explicit + # way is to specify it as a tuple consisting of a synthesis algorithm and a + # list of additional arguments, e.g., + # ("kms", {"all_mats": 1, "max_paths": 100, "orig_circuit": 0}), or + # ("pmh", {}). + # When the list of additional arguments is empty, one can also specify + # just the synthesis algorithm, e.g., + # "pmh". + if isinstance(method, tuple): + plugin_specifier, plugin_args = method + else: + plugin_specifier = method + plugin_args = {} + + # There are two ways to specify a synthesis algorithm being run, + # either by name, e.g. "kms" (which then should be specified in entry_points), + # or directly as a class inherited from HighLevelSynthesisPlugin (which then + # does not need to be specified in entry_points). + if isinstance(plugin_specifier, str): + if plugin_specifier not in hls_plugin_manager.method_names(op.name): + raise TranspilerError( + f"Specified method: {plugin_specifier} not found in available " + f"plugins for {op.name}" + ) + plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) + else: + plugin_method = plugin_specifier + + # Set the number of available clean and dirty auxiliary qubits via plugin args. + plugin_args["num_clean_ancillas"] = num_clean_ancillas + plugin_args["num_dirty_ancillas"] = num_dirty_ancillas + + decomposition = plugin_method.run( + op, + coupling_map=data.coupling_map, + target=data.target, + qubits=qubits, + **plugin_args, + ) + + # The synthesis methods that are not suited for the given higher-level-object + # will return None. + if decomposition is not None: + if data.hls_config.plugin_selection == "sequential": + # In the "sequential" mode the first successful decomposition is + # returned. + best_decomposition = decomposition + break + + # In the "run everything" mode we update the best decomposition + # discovered + current_score = data.hls_config.plugin_evaluation_fn(decomposition) + if current_score < best_score: + best_decomposition = decomposition + best_score = current_score + + return best_decomposition + + +def _apply_annotations( + data: HLSData, synthesized: Operation | QuantumCircuit, modifiers: list[Modifier] +) -> QuantumCircuit: + """ + Recursively synthesizes annotated operations. + Returns either the synthesized operation or None (which occurs when the operation + is not an annotated operation). + """ + for modifier in modifiers: + if isinstance(modifier, InverseModifier): + # Both QuantumCircuit and Gate have inverse method + synthesized = synthesized.inverse() + + elif isinstance(modifier, ControlModifier): + # Both QuantumCircuit and Gate have control method, however for circuits + # it is more efficient to avoid constructing the controlled quantum circuit. + if isinstance(synthesized, QuantumCircuit): + synthesized = synthesized.to_gate() + + synthesized = synthesized.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + annotated=False, ) - # The synthesis methods that are not suited for the given higher-level-object - # will return None. - if decomposition is not None: - if self.data.hls_config.plugin_selection == "sequential": - # In the "sequential" mode the first successful decomposition is - # returned. - best_decomposition = decomposition - break - - # In the "run everything" mode we update the best decomposition - # discovered - current_score = self.data.hls_config.plugin_evaluation_fn(decomposition) - if current_score < best_score: - best_decomposition = decomposition - best_score = current_score - - return best_decomposition - - def _apply_annotations( - self, synthesized: Operation | QuantumCircuit, modifiers: list[Modifier] - ) -> QuantumCircuit: - """ - Recursively synthesizes annotated operations. - Returns either the synthesized operation or None (which occurs when the operation - is not an annotated operation). - """ - for modifier in modifiers: - if isinstance(modifier, InverseModifier): - # Both QuantumCircuit and Gate have inverse method - synthesized = synthesized.inverse() - - elif isinstance(modifier, ControlModifier): - # Both QuantumCircuit and Gate have control method, however for circuits - # it is more efficient to avoid constructing the controlled quantum circuit. - if isinstance(synthesized, QuantumCircuit): - synthesized = synthesized.to_gate() - - synthesized = synthesized.control( - num_ctrl_qubits=modifier.num_ctrl_qubits, - label=None, - ctrl_state=modifier.ctrl_state, - annotated=False, + if isinstance(synthesized, AnnotatedOperation): + raise TranspilerError( + "HighLevelSynthesis failed to synthesize the control modifier." ) - if isinstance(synthesized, AnnotatedOperation): - raise TranspilerError( - "HighLevelSynthesis failed to synthesize the control modifier." - ) + elif isinstance(modifier, PowerModifier): + # QuantumCircuit has power method, and Gate needs to be converted + # to a quantum circuit. + if not isinstance(synthesized, QuantumCircuit): + synthesized = _instruction_to_circuit(synthesized) - elif isinstance(modifier, PowerModifier): - # QuantumCircuit has power method, and Gate needs to be converted - # to a quantum circuit. - if not isinstance(synthesized, QuantumCircuit): - synthesized = _instruction_to_circuit(synthesized) + synthesized = synthesized.power(modifier.power) - synthesized = synthesized.power(modifier.power) + else: + raise TranspilerError(f"Unknown modifier {modifier}.") - else: - raise TranspilerError(f"Unknown modifier {modifier}.") + return synthesized - return synthesized - def _definitely_skip_node( - self, node: DAGOpNode, qubits: tuple[int] | None, dag: DAGCircuit - ) -> bool: - """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will - attempt to synthesise it) without accessing its Python-space `Operation`. +def _definitely_skip_node( + data: HLSData, node: DAGOpNode, qubits: tuple[int] | None, dag: DAGCircuit +) -> bool: + """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will + attempt to synthesise it) without accessing its Python-space `Operation`. - This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to - avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the - node (which is _most_ nodes).""" + This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to + avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the + node (which is _most_ nodes).""" - if ( - dag._has_calibration_for(node) - or len(node.qargs) < self.data.min_qubits - or node.is_directive() - or (self._instruction_supported(node.name, qubits) and not node.is_control_flow()) - ): - return True - - return ( - # The fast path is just for Rust-space standard gates (which excludes - # `AnnotatedOperation`). - node.is_standard_gate() - # We don't have the fast-path for controlled gates over 3 or more qubits. - # However, we most probably want the fast-path for controlled 2-qubit gates - # (such as CX, CZ, CY, CH, CRX, and so on), so "_definitely_skip_node" should - # not immediately return False when encountering a controlled gate over 2 qubits. - and not (node.is_controlled_gate() and node.num_qubits >= 3) - # If there are plugins to try, they need to be tried. - and not self._methods_to_try(node.name) - # If all the above constraints hold, and it's already supported or the basis translator - # can handle it, we'll leave it be. - and ( - # This uses unfortunately private details of `EquivalenceLibrary`, but so does the - # `BasisTranslator`, and this is supposed to just be temporary til this is moved - # into Rust space. - self.data.equivalence_library is not None - and equivalence.Key(name=node.name, num_qubits=node.num_qubits) - in self.data.equivalence_library.keys() - ) + if ( + dag._has_calibration_for(node) + or len(node.qargs) < data.min_qubits + or node.is_directive() + or (_instruction_supported(data, node.name, qubits) and not node.is_control_flow()) + ): + return True + + return ( + # The fast path is just for Rust-space standard gates (which excludes + # `AnnotatedOperation`). + node.is_standard_gate() + # We don't have the fast-path for controlled gates over 3 or more qubits. + # However, we most probably want the fast-path for controlled 2-qubit gates + # (such as CX, CZ, CY, CH, CRX, and so on), so "_definitely_skip_node" should + # not immediately return False when encountering a controlled gate over 2 qubits. + and not (node.is_controlled_gate() and node.num_qubits >= 3) + # If there are plugins to try, they need to be tried. + and not _methods_to_try(data, node.name) + # If all the above constraints hold, and it's already supported or the basis translator + # can handle it, we'll leave it be. + and ( + # This uses unfortunately private details of `EquivalenceLibrary`, but so does the + # `BasisTranslator`, and this is supposed to just be temporary til this is moved + # into Rust space. + data.equivalence_library is not None + and equivalence.Key(name=node.name, num_qubits=node.num_qubits) + in data.equivalence_library.keys() ) + ) - def _instruction_supported(self, name: str, qubits: tuple[int] | None) -> bool: - # include path for when target exists but target.num_qubits is None (BasicSimulator) - if self.data.target is None or self.data.target.num_qubits is None: - return name in self.data.device_insts - return self.data.target.instruction_supported(operation_name=name, qargs=qubits) + +def _instruction_supported(data: HLSData, name: str, qubits: tuple[int] | None) -> bool: + # include path for when target exists but target.num_qubits is None (BasicSimulator) + if data.target is None or data.target.num_qubits is None: + return name in data.device_insts + return data.target.instruction_supported(operation_name=name, qargs=qubits) def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: From 2f2e1400229834f1b39053d7011be5f0544fef5f Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 16 Dec 2024 10:22:31 +0200 Subject: [PATCH 03/32] moving handling annotated operations to plugin The interface is now quite ugly, and we also have an extra isinstance check for each gate, we will see if it can be removed. --- pyproject.toml | 1 + .../passes/synthesis/high_level_synthesis.py | 157 ++++-------------- .../passes/synthesis/hls_plugins.py | 134 +++++++++++++++ 3 files changed, 168 insertions(+), 124 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bdabeda6f146..8f01584b7eff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "Multiplier.cumulative_h18" = "qiskit.transpiler.passes.synthesis.hls_plugins:MultiplierSynthesisH18" "PauliEvolution.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:PauliEvolutionSynthesisDefault" "PauliEvolution.rustiq" = "qiskit.transpiler.passes.synthesis.hls_plugins:PauliEvolutionSynthesisRustiq" +"annotated.default" = "qiskit.transpiler.passes.synthesis.hls_plugins:AnnotatedSynthesisDefault" [project.entry-points."qiskit.transpiler.init"] default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 99533c4b4e86..56adc57df738 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -519,92 +519,47 @@ def _synthesize_operation( 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: - # 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 - - # get qubits of base operation - control_qubits = qubits[0:num_ctrl] - - # Do not allow access to control qubits - tracker.disable(context.to_globals(control_qubits)) - synthesized_base_op, _ = _synthesize_operation( + # Try synthesis via HLS -- which will return ``None`` if unsuccessful. + indices = ( + qubits if data.use_qubit_indices or isinstance(operation, AnnotatedOperation) else None + ) + if len(hls_methods := _methods_to_try(data, 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 = _synthesize_op_using_plugins( data, - operation.base_op, - baseop_qubits, - tracker, - context, - use_ancillas=use_ancillas, + hls_methods, + operation, + indices, + num_clean_available, + num_dirty_available, + tracker=tracker, + context=context, ) - 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): + # 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_base_op.num_qubits - len(baseop_qubits), - context.to_globals(baseop_qubits), + 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) - # Restore access to control qubits. - tracker.enable(context.to_globals(control_qubits)) - # This step currently does not introduce ancilla qubits. - synthesized = _apply_annotations(data, synthesized_base_op, operation.modifiers) - - # If it was no AnnotatedOperation, try synthesizing via HLS or by unrolling. - else: - # Try synthesis via HLS -- which will return ``None`` if unsuccessful. - indices = qubits if data.use_qubit_indices else None - if len(hls_methods := _methods_to_try(data, 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 = _synthesize_op_using_plugins( - data, - hls_methods, - operation, - indices, - 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 data.top_level_only: - synthesized = _get_custom_definition(data, operation, indices) + # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. + if synthesized is None and not data.top_level_only: + synthesized = _get_custom_definition(data, operation, indices) if synthesized is None: # if we didn't synthesize, there was nothing to unroll @@ -719,6 +674,8 @@ def _synthesize_op_using_plugins( qubits: list[int] | None, num_clean_ancillas: int = 0, num_dirty_ancillas: int = 0, + tracker: QubitTracker = None, + context: QubitContext = None, ) -> QuantumCircuit | None: """ Attempts to synthesize op using plugin mechanism. @@ -768,6 +725,9 @@ def _synthesize_op_using_plugins( # Set the number of available clean and dirty auxiliary qubits via plugin args. plugin_args["num_clean_ancillas"] = num_clean_ancillas plugin_args["num_dirty_ancillas"] = num_dirty_ancillas + plugin_args["_qubit_tracker"] = tracker + plugin_args["_qubit_context"] = context + plugin_args["_data"] = data decomposition = plugin_method.run( op, @@ -796,51 +756,6 @@ def _synthesize_op_using_plugins( return best_decomposition -def _apply_annotations( - data: HLSData, synthesized: Operation | QuantumCircuit, modifiers: list[Modifier] -) -> QuantumCircuit: - """ - Recursively synthesizes annotated operations. - Returns either the synthesized operation or None (which occurs when the operation - is not an annotated operation). - """ - for modifier in modifiers: - if isinstance(modifier, InverseModifier): - # Both QuantumCircuit and Gate have inverse method - synthesized = synthesized.inverse() - - elif isinstance(modifier, ControlModifier): - # Both QuantumCircuit and Gate have control method, however for circuits - # it is more efficient to avoid constructing the controlled quantum circuit. - if isinstance(synthesized, QuantumCircuit): - synthesized = synthesized.to_gate() - - synthesized = synthesized.control( - num_ctrl_qubits=modifier.num_ctrl_qubits, - label=None, - ctrl_state=modifier.ctrl_state, - annotated=False, - ) - - if isinstance(synthesized, AnnotatedOperation): - raise TranspilerError( - "HighLevelSynthesis failed to synthesize the control modifier." - ) - - elif isinstance(modifier, PowerModifier): - # QuantumCircuit has power method, and Gate needs to be converted - # to a quantum circuit. - if not isinstance(synthesized, QuantumCircuit): - synthesized = _instruction_to_circuit(synthesized) - - synthesized = synthesized.power(modifier.power) - - else: - raise TranspilerError(f"Unknown modifier {modifier}.") - - return synthesized - - def _definitely_skip_node( data: HLSData, node: DAGOpNode, qubits: tuple[int] | None, dag: DAGCircuit ) -> bool: @@ -888,9 +803,3 @@ def _instruction_supported(data: HLSData, name: str, qubits: tuple[int] | None) if data.target is None or data.target.num_qubits is None: return name in data.device_insts return data.target.instruction_supported(operation_name=name, qargs=qubits) - - -def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: - circuit = QuantumCircuit(inst.num_qubits, inst.num_clbits) - circuit.append(inst, circuit.qubits, circuit.clbits) - return circuit diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index a609b11f0fef..c5374501ce1a 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -397,7 +397,11 @@ import numpy as np import rustworkx as rx +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.operation import Operation +from qiskit.circuit.instruction import Instruction from qiskit.circuit.library import ( LinearFunction, QFTGate, @@ -410,6 +414,13 @@ FullAdderGate, MultiplierGate, ) +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + Modifier, + ControlModifier, + InverseModifier, + PowerModifier, +) from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.coupling import CouplingMap @@ -1557,3 +1568,126 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** upto_phase=upto_phase, resynth_clifford_method=resynth_clifford_method, ) + + +class AnnotatedSynthesisDefault(HighLevelSynthesisPlugin): + """Synthesize :class:`.AnnotatedOperation`""" + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + # pylint: disable=cyclic-import + from .high_level_synthesis import _synthesize_operation + + if not isinstance(high_level_object, AnnotatedOperation): + return None + + operation = high_level_object + modifiers = high_level_object.modifiers + tracker = options.get("_qubit_tracker", None) + context = options.get("_qubit_context", None) + data = options.get("_data") + num_clean_ancillas = options.get("num_clean_ancillas", 0) + num_dirty_ancillas = options.get("num_dirty_ancillas", 0) + use_ancillas = (num_clean_ancillas + num_dirty_ancillas) > 0 + + if len(modifiers) > 0: + # 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 + + # get qubits of base operation + control_qubits = qubits[0:num_ctrl] + + # Do not allow access to control qubits + tracker.disable(context.to_globals(control_qubits)) + synthesized_base_op, _ = _synthesize_operation( + data, + 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 = _apply_annotations(data, synthesized_base_op, operation.modifiers) + return synthesized + + return None + + +def _apply_annotations( + data: "HLSData", synthesized: Operation | QuantumCircuit, modifiers: list[Modifier] +) -> QuantumCircuit: + """ + Recursively synthesizes annotated operations. + Returns either the synthesized operation or None (which occurs when the operation + is not an annotated operation). + """ + + for modifier in modifiers: + if isinstance(modifier, InverseModifier): + # Both QuantumCircuit and Gate have inverse method + synthesized = synthesized.inverse() + + elif isinstance(modifier, ControlModifier): + # Both QuantumCircuit and Gate have control method, however for circuits + # it is more efficient to avoid constructing the controlled quantum circuit. + if isinstance(synthesized, QuantumCircuit): + synthesized = synthesized.to_gate() + + synthesized = synthesized.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + annotated=False, + ) + + if isinstance(synthesized, AnnotatedOperation): + raise TranspilerError( + "HighLevelSynthesis failed to synthesize the control modifier." + ) + + elif isinstance(modifier, PowerModifier): + # QuantumCircuit has power method, and Gate needs to be converted + # to a quantum circuit. + if not isinstance(synthesized, QuantumCircuit): + synthesized = _instruction_to_circuit(synthesized) + + synthesized = synthesized.power(modifier.power) + + else: + raise TranspilerError(f"Unknown modifier {modifier}.") + + return synthesized + + +def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: + circuit = QuantumCircuit(inst.num_qubits, inst.num_clbits) + circuit.append(inst, circuit.qubits, circuit.clbits) + return circuit From 7db3493bc1f7a2409f85b2d4260c27eb07e3e8c8 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Sun, 22 Dec 2024 11:57:07 +0200 Subject: [PATCH 04/32] improving control-modified synthesis plugin --- .../passes/synthesis/hls_plugins.py | 46 ++++++++++++++++--- .../transpiler/test_high_level_synthesis.py | 9 ++-- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index c5374501ce1a..451e6d14ee3b 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -397,6 +397,7 @@ import numpy as np import rustworkx as rx +from qiskit.circuit.quantumregister import QuantumRegister from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.quantumcircuit import QuantumCircuit @@ -1636,6 +1637,8 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # This step currently does not introduce ancilla qubits. synthesized = _apply_annotations(data, synthesized_base_op, operation.modifiers) + assert isinstance(synthesized, QuantumCircuit) + return synthesized return None @@ -1658,15 +1661,39 @@ def _apply_annotations( elif isinstance(modifier, ControlModifier): # Both QuantumCircuit and Gate have control method, however for circuits # it is more efficient to avoid constructing the controlled quantum circuit. + + assert synthesized.num_clbits == 0 + + controlled_circ = QuantumCircuit(modifier.num_ctrl_qubits + synthesized.num_qubits) + if isinstance(synthesized, QuantumCircuit): - synthesized = synthesized.to_gate() + for inst in synthesized: + inst_op = inst.operation + inst_qubits = inst.qubits + controlled_op = inst_op.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + annotated=False, + ) + controlled_qubits = list(range(0, modifier.num_ctrl_qubits)) + [ + modifier.num_ctrl_qubits + synthesized.find_bit(q).index + for q in inst_qubits + ] + controlled_circ.append(controlled_op, controlled_qubits) + else: + + assert (synthesized, Operation) + synthesized = synthesized.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + annotated=False, + ) - synthesized = synthesized.control( - num_ctrl_qubits=modifier.num_ctrl_qubits, - label=None, - ctrl_state=modifier.ctrl_state, - annotated=False, - ) + controlled_circ.append(synthesized, controlled_circ.qubits) + + synthesized = controlled_circ if isinstance(synthesized, AnnotatedOperation): raise TranspilerError( @@ -1684,6 +1711,11 @@ def _apply_annotations( else: raise TranspilerError(f"Unknown modifier {modifier}.") + if not isinstance(synthesized, QuantumCircuit): + circuit = QuantumCircuit(synthesized.num_qubits) + circuit.append(synthesized, circuit.qubits) + return circuit + return synthesized diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 11aa8501afd0..cc3371ee479d 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -1106,10 +1106,8 @@ def test_control_clifford(self): cliff = Clifford(qc) circuit = QuantumCircuit(4) circuit.append(AnnotatedOperation(cliff, ControlModifier(2)), [0, 1, 2, 3]) - transpiled_circuit = HighLevelSynthesis()(circuit) - expected_circuit = QuantumCircuit(4) - expected_circuit.append(cliff.to_instruction().control(2), [0, 1, 2, 3]) - self.assertEqual(transpiled_circuit, expected_circuit) + transpiled_circuit = HighLevelSynthesis(basis_gates=["cx", "u"])(circuit) + self.assertEqual(transpiled_circuit.count_ops().keys(), {"cx", "u"}) def test_multiple_controls(self): """Test lazy controlled synthesis with multiple control modifiers.""" @@ -1128,6 +1126,9 @@ def test_nested_controls(self): circuit = QuantumCircuit(5) circuit.append(lazy_gate2, [0, 1, 2, 3, 4]) transpiled_circuit = HighLevelSynthesis()(circuit) + print(transpiled_circuit) + print(type(transpiled_circuit[0])) + print(transpiled_circuit[0]) expected_circuit = QuantumCircuit(5) expected_circuit.append(SwapGate().control(2).control(1), [0, 1, 2, 3, 4]) self.assertEqual(transpiled_circuit, expected_circuit) From 0a37feeed57c7dab0defa14ed2a94d07af7cbe1a Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Sun, 22 Dec 2024 15:15:21 +0200 Subject: [PATCH 05/32] Changing internal functions to work with QuantumCircuits instead of DAGCircuits --- crates/accelerate/src/high_level_synthesis.rs | 19 ++ .../passes/synthesis/high_level_synthesis.py | 204 ++++++++++++------ .../circuit/library/test_multipliers.py | 5 +- 3 files changed, 159 insertions(+), 69 deletions(-) diff --git a/crates/accelerate/src/high_level_synthesis.rs b/crates/accelerate/src/high_level_synthesis.rs index 74bf5370ba72..8307b7b2ef40 100644 --- a/crates/accelerate/src/high_level_synthesis.rs +++ b/crates/accelerate/src/high_level_synthesis.rs @@ -243,6 +243,25 @@ impl QubitContext { fn to_globals(&self, qubits: Vec) -> Vec { qubits.iter().map(|q| self.local_to_global[*q]).collect() } + + /// Pretty-prints + pub fn __str__(&self) -> String { + let mut out = String::from("QubitContext("); + for (q_loc, q_glob ) in self.local_to_global.iter().enumerate() { + out.push_str(&q_loc.to_string()); + out.push(':'); + out.push(' '); + out.push_str(&q_glob.to_string()); + + if q_loc != self.local_to_global.len() - 1 { + out.push(';'); + out.push(' '); + } else { + out.push(')'); + } + } + out + } } pub fn high_level_synthesis_mod(m: &Bound) -> PyResult<()> { diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 56adc57df738..5ca4e99788e7 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -23,6 +23,7 @@ import numpy as np from qiskit.circuit.annotated_operation import Modifier +from qiskit.circuit.controlflow.control_flow import ControlFlowOp from qiskit.circuit.operation import Operation from qiskit.circuit.instruction import Instruction from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -303,24 +304,39 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: TranspilerError: when the transpiler is unable to synthesize the given DAG (for instance, when the specified synthesis method is not available). """ + + # 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. + for node in dag.op_nodes(): + qubits = tuple(dag.find_bit(q).index for q in node.qargs) + if not _definitely_skip_node(self.data, node, qubits, dag): + break + else: + # The for-loop terminates without reaching the break statement + return dag + + qubits = tuple(dag.find_bit(q).index for q in dag.qubits) context = QubitContext(list(range(len(dag.qubits)))) tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.data.qubits_initially_zero: tracker.set_clean(context.to_globals(qubits)) - out_dag = _run(dag, self.data, tracker, context, use_ancillas=True, top_level=True) + # ToDo: try to avoid this conversion + circuit = dag_to_circuit(dag) + out_circuit = _run(circuit, self.data, tracker, context, use_ancillas=True) + assert isinstance(out_circuit, QuantumCircuit) + out_dag = circuit_to_dag(out_circuit) return out_dag def _run( - dag: DAGCircuit, + dag: QuantumCircuit, data: HLSData, tracker: QubitTracker, context: QubitContext, use_ancillas: bool, - top_level: bool, -) -> DAGCircuit: +) -> QuantumCircuit: """ The main recursive function that synthesizes a DAGCircuit. @@ -329,7 +345,6 @@ def _run( 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. The function returns the synthesized DAG. @@ -339,22 +354,13 @@ def _run( context. """ - if dag.num_qubits() != context.num_qubits(): - raise TranspilerError("HighLevelSynthesis internal error.") + assert isinstance(dag, QuantumCircuit) - # 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 _definitely_skip_node(data, 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 + if dag.num_qubits != context.num_qubits(): + raise TranspilerError("HighLevelSynthesis internal error.") + + # 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 @@ -368,8 +374,9 @@ def _run( # 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) + for (idx, inst) in enumerate(dag): + op = inst.operation + qubits = tuple(dag.find_bit(q).index for q in inst.qubits) processed = False synthesized = None synthesized_context = None @@ -377,33 +384,38 @@ def _run( # 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"]: + if op.name in ["id", "delay", "barrier"]: # tracker not updated, these are no-ops processed = True - elif node.op.name == "reset": + elif 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 - elif _definitely_skip_node(data, node, qubits, dag): + elif _definitely_skip_op(data, op, qubits, dag): tracker.set_dirty(context.to_globals(qubits)) # next check control flow - elif node.is_control_flow(): + elif isinstance(op, ControlFlowOp): + # print("I AM HERE") + # print(f"CONTEXT: {context}") + # print(f"TRACKER: {tracker}") inner_context = context.restrict(qubits) - synthesized = control_flow.map_blocks( - partial( + # print(f"INNER_CONTEXT: {inner_context}") + circuit_mapping = partial( _run, data=data, tracker=tracker, context=inner_context, use_ancillas=False, - top_level=False, - ), - node.op, - ) + ) + synthesized = op.replace_blocks([circuit_mapping(block) for block in op.blocks]) + # print(f"SYNTHESIZED: {synthesized}") + # synthesized = _wrap_in_circuit(synthesized) + # synthesized_context = context + # now we are free to synthesize else: @@ -412,12 +424,12 @@ def _run( # 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 = _synthesize_operation( - data, node.op, qubits, tracker, context, use_ancillas=use_ancillas + data, op, qubits, tracker, context, use_ancillas=use_ancillas ) # If the synthesis changed the operation (i.e. it is not None), store the result. if synthesized is not None: - synthesized_nodes[node._node_id] = (synthesized, synthesized_context) + synthesized_nodes[idx] = (synthesized, synthesized_context) # If the synthesis did not change anything, just update the qubit tracker. elif not processed: @@ -425,56 +437,62 @@ def _run( # We did not change anything just return the input. if len(synthesized_nodes) == 0: - if dag.num_qubits() != context.num_qubits(): + if dag.num_qubits != context.num_qubits(): raise TranspilerError("HighLevelSynthesis internal error.") + assert isinstance(dag, QuantumCircuit) return dag # 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() - num_additional_qubits = context.num_qubits() - out.num_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)]) + out.add_bits([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(): + for (idx, inst) in enumerate(dag): + op = inst.operation - if op_tuple := synthesized_nodes.get(node._node_id, None): + if op_tuple := synthesized_nodes.get(idx, None): op, op_context = op_tuple if isinstance(op, Operation): - out.apply_operation_back(op, node.qargs, node.cargs) + # We sgould not be here + # assert False + # out.apply_operation_back(op, node.qargs, node.cargs) + # print(f"{inst.qubits = }, {inst.clbits = }") + out.append(op, inst.qubits, inst.clbits) continue - if isinstance(op, QuantumCircuit): - op = circuit_to_dag(op, copy_operations=False) + assert isinstance(op, QuantumCircuit) inner_to_global = op_context.to_global_mapping() - if isinstance(op, DAGCircuit): - qubit_map = { - 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, - tuple(qubit_map[qarg] for qarg in sub_node.qargs), - tuple(clbit_map[carg] for carg in sub_node.cargs), - ) - out.global_phase += op.global_phase - - else: - raise TranspilerError(f"Unexpected synthesized type: {type(op)}") + qubit_map = { + q: index_to_qubit[outer_to_local[inner_to_global[i]]] + for (i, q) in enumerate(op.qubits) + } + clbit_map = dict(zip(op.clbits, inst.clbits)) + + for sub_node in op: + out.append( + sub_node.operation, + tuple(qubit_map[qarg] for qarg in sub_node.qubits), + tuple(clbit_map[carg] for carg in sub_node.clbits), + ) + out.global_phase += op.global_phase + + # else: + # raise TranspilerError(f"Unexpected synthesized type: {type(op)}") else: - out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + out.append(op, inst.qubits, inst.clbits) - if out.num_qubits() != context.num_qubits(): + + assert isinstance(out, QuantumCircuit) + if out.num_qubits != context.num_qubits(): raise TranspilerError("HighLevelSynthesis internal error.") return out @@ -487,7 +505,7 @@ def _synthesize_operation( tracker: QubitTracker, context: QubitContext, use_ancillas: bool, -) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, QubitContext | None]: +) -> tuple[QuantumCircuit | 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 @@ -568,6 +586,9 @@ def _synthesize_operation( # if it has been synthesized, recurse and finally store the decomposition elif isinstance(synthesized, Operation): + # we should no longer be here! + assert False + resynthesized, resynthesized_context = _synthesize_operation( data, synthesized, qubits, tracker, context, use_ancillas=use_ancillas ) @@ -585,10 +606,10 @@ def _synthesize_operation( # 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) + as_dag = synthesized inner_context = context.restrict(qubits) - if as_dag.num_qubits() != inner_context.num_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 @@ -596,14 +617,14 @@ def _synthesize_operation( # which ancilla qubits will be allocated. saved_tracker = tracker.copy() synthesized = _run( - as_dag, data, tracker, inner_context, use_ancillas=use_ancillas, top_level=False + as_dag, data, tracker, inner_context, use_ancillas=use_ancillas, ) synthesized_context = inner_context - if (synthesized is not None) and (synthesized.num_qubits() > len(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) + synthesized.num_qubits - len(qubits), context.to_globals(qubits) ) global_to_local = context.to_local_mapping() @@ -623,6 +644,7 @@ def _synthesize_operation( if isinstance(synthesized, DAGCircuit) and synthesized_context is None: raise TranspilerError("HighLevelSynthesis internal error.") + assert synthesized is None or isinstance(synthesized, QuantumCircuit) return synthesized, synthesized_context @@ -798,8 +820,58 @@ def _definitely_skip_node( ) +# ToDo: try to avoid duplication with other function +def _definitely_skip_op( + data: HLSData, op: Operation, qubits: tuple[int], dag: DAGCircuit +) -> bool: + """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will + attempt to synthesise it) without accessing its Python-space `Operation`. + + This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to + avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the + node (which is _most_ nodes).""" + + assert qubits is not None + + if ( + len(qubits) < data.min_qubits + or getattr(op, "_directive", False) + or (_instruction_supported(data, op.name, qubits) and not isinstance(op, ControlFlowOp)) + ): + return True + + return ( + # The fast path is just for Rust-space standard gates (which excludes + # `AnnotatedOperation`). + getattr(op, "_standard_gate", False) + # We don't have the fast-path for controlled gates over 3 or more qubits. + # However, we most probably want the fast-path for controlled 2-qubit gates + # (such as CX, CZ, CY, CH, CRX, and so on), so "_definitely_skip_node" should + # not immediately return False when encountering a controlled gate over 2 qubits. + and not (isinstance(op, ControlFlowOp) and op.num_qubits >= 3) + # If there are plugins to try, they need to be tried. + and not _methods_to_try(data, op.name) + # If all the above constraints hold, and it's already supported or the basis translator + # can handle it, we'll leave it be. + and ( + # This uses unfortunately private details of `EquivalenceLibrary`, but so does the + # `BasisTranslator`, and this is supposed to just be temporary til this is moved + # into Rust space. + data.equivalence_library is not None + and equivalence.Key(name=op.name, num_qubits=len(qubits)) + in data.equivalence_library.keys() + ) + ) + + def _instruction_supported(data: HLSData, name: str, qubits: tuple[int] | None) -> bool: # include path for when target exists but target.num_qubits is None (BasicSimulator) if data.target is None or data.target.num_qubits is None: return name in data.device_insts return data.target.instruction_supported(operation_name=name, qargs=qubits) + + +def _wrap_in_circuit(op: Operation) -> QuantumCircuit: + circuit = QuantumCircuit(op.num_qubits, op.num_clbits) + circuit.append(op, circuit.qubits, circuit.clbits) + return circuit \ No newline at end of file diff --git a/test/python/circuit/library/test_multipliers.py b/test/python/circuit/library/test_multipliers.py index f5665007079b..f6301470c5a5 100644 --- a/test/python/circuit/library/test_multipliers.py +++ b/test/python/circuit/library/test_multipliers.py @@ -143,7 +143,7 @@ def test_plugins(self): """Test setting the HLS plugins for the modular adder.""" # all gates with the plugins we check, including an expected operation - plugins = [("cumulative_h18", "ccircuit-.*"), ("qft_r17", "qft")] + plugins = [("cumulative_h18", "ch"), ("qft_r17", "mcphase")] num_state_qubits = 2 @@ -159,8 +159,7 @@ def test_plugins(self): synth = hls(circuit) ops = set(synth.count_ops().keys()) - - self.assertTrue(any(re.match(expected_op, op) for op in ops)) + self.assertIn(expected_op, ops) if __name__ == "__main__": From a74f68e895af3bc56120053ca2d13691bec8619e Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Sun, 22 Dec 2024 15:23:40 +0200 Subject: [PATCH 06/32] renaming --- .../passes/synthesis/high_level_synthesis.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 5ca4e99788e7..d02b543e30e8 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -331,52 +331,52 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: def _run( - dag: QuantumCircuit, + input_circuit: QuantumCircuit, data: HLSData, tracker: QubitTracker, context: QubitContext, use_ancillas: bool, ) -> QuantumCircuit: """ - The main recursive function that synthesizes a DAGCircuit. + The main recursive function that synthesizes a QuantumCircuit. Input: - dag: the DAG to be synthesized. + circuit: the circuit 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. + context: the correspondence between the circuit's qubits and the global qubits. use_ancillas: if True, synthesis algorithms are allowed to use ancillas. - The function returns the synthesized DAG. + The function returns the synthesized QuantumCircuit. - 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, + Note that by using the auxiliary qubits to synthesize operations present in the input circuit, + the synthesized circuit may be defined over more qubits than the input circuit. In this case, the function update in-place the global qubits tracker and extends the local-to-global context. """ - assert isinstance(dag, QuantumCircuit) + assert isinstance(input_circuit, QuantumCircuit) - if dag.num_qubits != context.num_qubits(): + if input_circuit.num_qubits != context.num_qubits(): raise TranspilerError("HighLevelSynthesis internal error.") - # STEP 2: Analyze the nodes in the DAG. For each node in the DAG that needs + # STEP 2: Analyze the nodes in the circuit. For each node in the circuit 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 + # instance, the result of synthesizing a custom gate is a QuantumCircuit 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). + # circuit, 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. + # It does not distribute ancilla qubits between different operations present in the circuit. synthesized_nodes = {} - for (idx, inst) in enumerate(dag): + for (idx, inst) in enumerate(input_circuit): op = inst.operation - qubits = tuple(dag.find_bit(q).index for q in inst.qubits) + qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) processed = False synthesized = None synthesized_context = None @@ -394,7 +394,7 @@ def _run( processed = True # check if synthesis for the operation can be skipped - elif _definitely_skip_op(data, op, qubits, dag): + elif _definitely_skip_op(data, op, qubits, input_circuit): tracker.set_dirty(context.to_globals(qubits)) # next check control flow @@ -420,9 +420,9 @@ def _run( # now we are free to synthesize else: # 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. + # a circuit, it's the correspondence between its qubits and the global qubits). + # Also note that the circuit may use auxiliary qubits. The qubits tracker and the + # current circuit's context are updated in-place. synthesized, synthesized_context = _synthesize_operation( data, op, qubits, tracker, context, use_ancillas=use_ancillas ) @@ -437,15 +437,15 @@ def _run( # We did not change anything just return the input. if len(synthesized_nodes) == 0: - if dag.num_qubits != context.num_qubits(): + if input_circuit.num_qubits != context.num_qubits(): raise TranspilerError("HighLevelSynthesis internal error.") - assert isinstance(dag, QuantumCircuit) - return dag + assert isinstance(input_circuit, QuantumCircuit) + return input_circuit - # STEP 3. We rebuild the DAG with new operations. Note that we could also + # STEP 3. We rebuild the circuit 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() + out = input_circuit.copy_empty_like() num_additional_qubits = context.num_qubits() - out.num_qubits if num_additional_qubits > 0: @@ -454,7 +454,7 @@ def _run( index_to_qubit = dict(enumerate(out.qubits)) outer_to_local = context.to_local_mapping() - for (idx, inst) in enumerate(dag): + for (idx, inst) in enumerate(input_circuit): op = inst.operation if op_tuple := synthesized_nodes.get(idx, None): From 3fe0454d2e7486d958c72501212f7d46247c0b98 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Sun, 22 Dec 2024 16:28:52 +0200 Subject: [PATCH 07/32] Removing unnecessary argument use_ancillas This argument is not needed as the qubit tracker already restricts allowed ancilla qubits. --- .../passes/synthesis/high_level_synthesis.py | 29 ++++--------------- .../passes/synthesis/hls_plugins.py | 8 ++--- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index d02b543e30e8..c5f2d0db5378 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -324,7 +324,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # ToDo: try to avoid this conversion circuit = dag_to_circuit(dag) - out_circuit = _run(circuit, self.data, tracker, context, use_ancillas=True) + out_circuit = _run(circuit, self.data, tracker, context) assert isinstance(out_circuit, QuantumCircuit) out_dag = circuit_to_dag(out_circuit) return out_dag @@ -335,7 +335,6 @@ def _run( data: HLSData, tracker: QubitTracker, context: QubitContext, - use_ancillas: bool, ) -> QuantumCircuit: """ The main recursive function that synthesizes a QuantumCircuit. @@ -344,7 +343,6 @@ def _run( circuit: the circuit to be synthesized. tracker: the global tracker, tracking the state of original qubits. context: the correspondence between the circuit's qubits and the global qubits. - use_ancillas: if True, synthesis algorithms are allowed to use ancillas. The function returns the synthesized QuantumCircuit. @@ -409,7 +407,6 @@ def _run( data=data, tracker=tracker, context=inner_context, - use_ancillas=False, ) synthesized = op.replace_blocks([circuit_mapping(block) for block in op.blocks]) # print(f"SYNTHESIZED: {synthesized}") @@ -424,7 +421,7 @@ def _run( # Also note that the circuit may use auxiliary qubits. The qubits tracker and the # current circuit's context are updated in-place. synthesized, synthesized_context = _synthesize_operation( - data, op, qubits, tracker, context, use_ancillas=use_ancillas + data, op, qubits, tracker, context, ) # If the synthesis changed the operation (i.e. it is not None), store the result. @@ -504,7 +501,6 @@ def _synthesize_operation( qubits: tuple[int], tracker: QubitTracker, context: QubitContext, - use_ancillas: bool, ) -> tuple[QuantumCircuit | None, QubitContext | None]: """ Synthesizes an operation. The function receives the qubits on which the operation @@ -542,12 +538,8 @@ def _synthesize_operation( qubits if data.use_qubit_indices or isinstance(operation, AnnotatedOperation) else None ) if len(hls_methods := _methods_to_try(data, 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 + num_clean_available = tracker.num_clean(context.to_globals(qubits)) + num_dirty_available = tracker.num_dirty(context.to_globals(qubits)) synthesized = _synthesize_op_using_plugins( data, @@ -589,17 +581,6 @@ def _synthesize_operation( # we should no longer be here! assert False - resynthesized, resynthesized_context = _synthesize_operation( - data, synthesized, qubits, tracker, context, use_ancillas=use_ancillas - ) - - 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 @@ -617,7 +598,7 @@ def _synthesize_operation( # which ancilla qubits will be allocated. saved_tracker = tracker.copy() synthesized = _run( - as_dag, data, tracker, inner_context, use_ancillas=use_ancillas, + as_dag, data, tracker, inner_context, ) synthesized_context = inner_context diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index b790dcd1f49e..9801918f4bb1 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1653,9 +1653,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** tracker = options.get("_qubit_tracker", None) context = options.get("_qubit_context", None) data = options.get("_data") - num_clean_ancillas = options.get("num_clean_ancillas", 0) - num_dirty_ancillas = options.get("num_dirty_ancillas", 0) - use_ancillas = (num_clean_ancillas + num_dirty_ancillas) > 0 if len(modifiers) > 0: # Note: the base operation must be synthesized without using potential control qubits @@ -1676,13 +1673,12 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** 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) + + assert not isinstance(synthesized_base_op, DAGCircuit) # Handle the case that synthesizing the base operation introduced # additional qubits (e.g. the base operation is a circuit that includes From 71fbf5766bec513656c0d04c54f7bf70634314dd Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 23 Dec 2024 15:39:25 +0200 Subject: [PATCH 08/32] progress --- crates/accelerate/src/high_level_synthesis.rs | 4 + .../passes/synthesis/high_level_synthesis.py | 191 +++++++++++------- .../passes/synthesis/hls_plugins.py | 5 +- 3 files changed, 129 insertions(+), 71 deletions(-) diff --git a/crates/accelerate/src/high_level_synthesis.rs b/crates/accelerate/src/high_level_synthesis.rs index 8307b7b2ef40..2a43d696beda 100644 --- a/crates/accelerate/src/high_level_synthesis.rs +++ b/crates/accelerate/src/high_level_synthesis.rs @@ -42,6 +42,10 @@ impl QubitTracker { } } + fn num_qubits(&self) -> usize { + self.num_qubits + } + /// Sets state of the given qubits to dirty fn set_dirty(&mut self, qubits: Vec) { for q in qubits { diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c5f2d0db5378..f219d4a4d04d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -324,9 +324,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # ToDo: try to avoid this conversion circuit = dag_to_circuit(dag) - out_circuit = _run(circuit, self.data, tracker, context) - assert isinstance(out_circuit, QuantumCircuit) - out_dag = circuit_to_dag(out_circuit) + input_qubits = list(range(circuit.num_qubits)) + (output_circuit, output_qubits) = _run(circuit, self.data, tracker, context, input_qubits) + assert isinstance(output_circuit, QuantumCircuit) + out_dag = circuit_to_dag(output_circuit) return out_dag @@ -335,7 +336,8 @@ def _run( data: HLSData, tracker: QubitTracker, context: QubitContext, -) -> QuantumCircuit: + input_qubits: tuple[int], +) -> tuple[QuantumCircuit, tuple[int]]: """ The main recursive function that synthesizes a QuantumCircuit. @@ -353,6 +355,7 @@ def _run( """ assert isinstance(input_circuit, QuantumCircuit) + # assert context.to_globals(range(input_circuit.num_qubits)) == input_qubits if input_circuit.num_qubits != context.num_qubits(): @@ -372,9 +375,27 @@ def _run( # It does not distribute ancilla qubits between different operations present in the circuit. synthesized_nodes = {} + global_to_local = dict() + for i, q in enumerate(input_qubits): + global_to_local[q] = i + + output_qubits = input_qubits + num_output_qubits = len(input_qubits) + + assert input_circuit.num_qubits == len(input_qubits) + print(f"===> {input_circuit.num_qubits = }, {input_qubits = }") + for (idx, inst) in enumerate(input_circuit): op = inst.operation - qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) + + # local qubit index (within the current input_circuit) + op_qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) + + print(f"I AM HERE 15: {input_qubits = }, {context}, {op = }, {op_qubits = }") + + # global qubits + op_global_qubits = [input_qubits[q] for q in op_qubits] + processed = False synthesized = None synthesized_context = None @@ -388,27 +409,34 @@ def _run( elif op.name == "reset": # reset qubits to 0 - tracker.set_clean(context.to_globals(qubits)) + tracker.set_clean(op_global_qubits) processed = True # check if synthesis for the operation can be skipped - elif _definitely_skip_op(data, op, qubits, input_circuit): - tracker.set_dirty(context.to_globals(qubits)) + elif _definitely_skip_op(data, op, op_global_qubits, input_circuit): + tracker.set_dirty(op_global_qubits) # next check control flow elif isinstance(op, ControlFlowOp): # print("I AM HERE") # print(f"CONTEXT: {context}") # print(f"TRACKER: {tracker}") - inner_context = context.restrict(qubits) - # print(f"INNER_CONTEXT: {inner_context}") - circuit_mapping = partial( - _run, - data=data, - tracker=tracker, - context=inner_context, - ) - synthesized = op.replace_blocks([circuit_mapping(block) for block in op.blocks]) + + new_blocks = [] + inner_context = context.restrict(op_qubits) + block_tracker=tracker.copy() + block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_global_qubits]) + + for block in op.blocks: + # print("=================") + # print(block) + # print(f"{op.num_qubits = }") + # print(f"{block.num_qubits = }") + # print(f"{op_global_qubits = }") + new_block = _run(block, data=data, tracker=block_tracker, context=inner_context, input_qubits=op_global_qubits)[0] + new_blocks.append(new_block) + synthesized = op.replace_blocks(new_blocks) + op_output_qubits = op_global_qubits # print(f"SYNTHESIZED: {synthesized}") # synthesized = _wrap_in_circuit(synthesized) # synthesized_context = context @@ -420,45 +448,55 @@ def _run( # a circuit, it's the correspondence between its qubits and the global qubits). # Also note that the circuit may use auxiliary qubits. The qubits tracker and the # current circuit's context are updated in-place. - synthesized, synthesized_context = _synthesize_operation( - data, op, qubits, tracker, context, + synthesized, synthesized_context, op_output_qubits = _synthesize_operation( + data, op, op_qubits, tracker, context, op_global_qubits, ) + print(f"{op = }, {op_output_qubits = }") + + for q in op_output_qubits: + if q not in global_to_local: + global_to_local[q] = num_output_qubits + num_output_qubits += 1 + output_qubits.append(q) + # If the synthesis changed the operation (i.e. it is not None), store the result. if synthesized is not None: - synthesized_nodes[idx] = (synthesized, synthesized_context) + synthesized_nodes[idx] = (synthesized, op_output_qubits) # If the synthesis did not change anything, just update the qubit tracker. elif not processed: - tracker.set_dirty(context.to_globals(qubits)) + tracker.set_dirty(op_global_qubits) # We did not change anything just return the input. if len(synthesized_nodes) == 0: - if input_circuit.num_qubits != context.num_qubits(): + print(f"I AM HERE 10: {input_circuit.num_qubits = }, {len(input_qubits) = }") + if input_circuit.num_qubits != len(input_qubits): raise TranspilerError("HighLevelSynthesis internal error.") + assert isinstance(input_circuit, QuantumCircuit) - return input_circuit + return (input_circuit, input_qubits) # STEP 3. We rebuild the circuit 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 = input_circuit.copy_empty_like() - num_additional_qubits = context.num_qubits() - out.num_qubits + num_additional_qubits = num_output_qubits - out.num_qubits if num_additional_qubits > 0: out.add_bits([Qubit() for _ in range(num_additional_qubits)]) index_to_qubit = dict(enumerate(out.qubits)) - outer_to_local = context.to_local_mapping() for (idx, inst) in enumerate(input_circuit): op = inst.operation if op_tuple := synthesized_nodes.get(idx, None): - op, op_context = op_tuple + op, op_output_qubits = op_tuple if isinstance(op, Operation): - # We sgould not be here + # We are here for controlled flow ops + # We should not be here # assert False # out.apply_operation_back(op, node.qargs, node.cargs) # print(f"{inst.qubits = }, {inst.clbits = }") @@ -467,9 +505,8 @@ def _run( assert isinstance(op, QuantumCircuit) - inner_to_global = op_context.to_global_mapping() qubit_map = { - q: index_to_qubit[outer_to_local[inner_to_global[i]]] + q: index_to_qubit[global_to_local[op_output_qubits[i]]] for (i, q) in enumerate(op.qubits) } clbit_map = dict(zip(op.clbits, inst.clbits)) @@ -482,17 +519,17 @@ def _run( ) out.global_phase += op.global_phase - # else: - # raise TranspilerError(f"Unexpected synthesized type: {type(op)}") + else: out.append(op, inst.qubits, inst.clbits) assert isinstance(out, QuantumCircuit) - if out.num_qubits != context.num_qubits(): + print(f"I AM HERE: {out.num_qubits = }, {len(output_qubits) = }") + if out.num_qubits != len(output_qubits): raise TranspilerError("HighLevelSynthesis internal error.") - return out + return (out, output_qubits) def _synthesize_operation( @@ -501,7 +538,8 @@ def _synthesize_operation( qubits: tuple[int], tracker: QubitTracker, context: QubitContext, -) -> tuple[QuantumCircuit | None, QubitContext | None]: + input_qubits: tuple[int], +) -> tuple[QuantumCircuit | None, QubitContext | None, tuple[int]]: """ 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 @@ -514,8 +552,9 @@ def _synthesize_operation( of where these ancilla qubits maps to). """ + assert operation.num_qubits == len(input_qubits) synthesized_context = None - + print(f"OK_SO_FAR: {operation.num_qubits = }, {len(input_qubits) = }") # 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) @@ -532,6 +571,7 @@ def _synthesize_operation( qubits = list(qubits) synthesized = None + output_qubits = input_qubits # Try synthesis via HLS -- which will return ``None`` if unsuccessful. indices = ( @@ -541,66 +581,72 @@ def _synthesize_operation( num_clean_available = tracker.num_clean(context.to_globals(qubits)) num_dirty_available = tracker.num_dirty(context.to_globals(qubits)) - synthesized = _synthesize_op_using_plugins( + res = _synthesize_op_using_plugins( data, hls_methods, operation, indices, + input_qubits, num_clean_available, num_dirty_available, tracker=tracker, context=context, ) + print(f"HERE: {operation = }, {type(res) = }, {res = }") + (synthesized, _) = res + # 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() + if synthesized is not None: + print(f"=> OK?: {synthesized.num_qubits = }, {len(qubits) = }, {len(output_qubits) = }") + if 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) + 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 aq not in output_qubits: + output_qubits.append(aq) + + if synthesized is not None: + print(f"WHAT IS GOING ON: {synthesized.num_qubits = }, {len(output_qubits) = }") + assert synthesized.num_qubits == len(output_qubits) # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. if synthesized is None and not data.top_level_only: - synthesized = _get_custom_definition(data, operation, indices) + synthesized, output_qubits = _get_custom_definition(data, operation, indices, input_qubits) if synthesized is None: # 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): - # we should no longer be here! - assert False - - elif isinstance(synthesized, QuantumCircuit): + else: + assert 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 = synthesized inner_context = context.restrict(qubits) - if as_dag.num_qubits != inner_context.num_qubits(): + if synthesized.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 = _run( - as_dag, data, tracker, inner_context, - ) + synthesized, output_qubits = _run(synthesized, data, tracker, inner_context, output_qubits) synthesized_context = inner_context + print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") + if (synthesized is not None) and (synthesized.num_qubits > len(qubits)): # need to borrow more qubits from tracker @@ -619,19 +665,20 @@ def _synthesize_operation( if len(qubits) > num_original_qubits: tracker.replace_state(saved_tracker, context.to_globals(qubits[num_original_qubits:])) - else: - raise TranspilerError(f"Unexpected synthesized type: {type(synthesized)}") + if isinstance(synthesized, DAGCircuit) and synthesized_context is None: raise TranspilerError("HighLevelSynthesis internal error.") assert synthesized is None or isinstance(synthesized, QuantumCircuit) - return synthesized, synthesized_context + return synthesized, synthesized_context, output_qubits + + def _get_custom_definition( - data: HLSData, inst: Instruction, qubits: list[int] | None -) -> QuantumCircuit | None: + data: HLSData, inst: Instruction, qubits: list[int] | None, input_qubits: tuple[int] +) -> tuple[QuantumCircuit | None, tuple[int]]: # check if the operation is already supported natively if not (isinstance(inst, ControlledGate) and inst._open_ctrl): # include path for when target exists but target.num_qubits is None (BasicSimulator) @@ -639,7 +686,7 @@ def _get_custom_definition( if inst_supported or ( data.equivalence_library is not None and data.equivalence_library.has_entry(inst) ): - return None # we support this operation already + return (None, input_qubits) # we support this operation already # if not, try to get the definition try: @@ -650,7 +697,7 @@ def _get_custom_definition( if definition is None: raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {inst}.") - return definition + return (definition, input_qubits) def _methods_to_try(data: HLSData, name: str): @@ -675,11 +722,13 @@ def _synthesize_op_using_plugins( hls_methods: list, op: Operation, qubits: list[int] | None, + input_qubits: tuple[int], + num_clean_ancillas: int = 0, num_dirty_ancillas: int = 0, tracker: QubitTracker = None, context: QubitContext = None, -) -> QuantumCircuit | None: +) -> tuple[QuantumCircuit | None, tuple[int]]: """ Attempts to synthesize op using plugin mechanism. @@ -731,6 +780,7 @@ def _synthesize_op_using_plugins( plugin_args["_qubit_tracker"] = tracker plugin_args["_qubit_context"] = context plugin_args["_data"] = data + plugin_args["input_qubits"] = input_qubits decomposition = plugin_method.run( op, @@ -756,7 +806,8 @@ def _synthesize_op_using_plugins( best_decomposition = decomposition best_score = current_score - return best_decomposition + # FIXME: + return (best_decomposition, None) def _definitely_skip_node( diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 9801918f4bb1..37d4221dd7c5 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1625,6 +1625,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** preserve_order = options.get("preserve_order", True) upto_clifford = options.get("upto_clifford", False) upto_phase = options.get("upto_phase", False) + input_qubits = options.get("input_qubits") resynth_clifford_method = options.get("resynth_clifford_method", 1) return synth_pauli_network_rustiq( @@ -1653,6 +1654,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** tracker = options.get("_qubit_tracker", None) context = options.get("_qubit_context", None) data = options.get("_data") + input_qubits = options.get("input_qubits") if len(modifiers) > 0: # Note: the base operation must be synthesized without using potential control qubits @@ -1667,12 +1669,13 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # Do not allow access to control qubits tracker.disable(context.to_globals(control_qubits)) - synthesized_base_op, _ = _synthesize_operation( + synthesized_base_op, _, _ = _synthesize_operation( data, operation.base_op, baseop_qubits, tracker, context, + input_qubits[num_ctrl:] ) if synthesized_base_op is None: From 89b33ac61333e78f457ce73709eef5dc34d8e562 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 23 Dec 2024 15:56:51 +0200 Subject: [PATCH 09/32] minor --- .../passes/synthesis/high_level_synthesis.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index f219d4a4d04d..eb07c349a556 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -318,13 +318,15 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: qubits = tuple(dag.find_bit(q).index for q in dag.qubits) context = QubitContext(list(range(len(dag.qubits)))) - tracker = QubitTracker(num_qubits=dag.num_qubits()) - if self.data.qubits_initially_zero: - tracker.set_clean(context.to_globals(qubits)) + # ToDo: try to avoid this conversion circuit = dag_to_circuit(dag) input_qubits = list(range(circuit.num_qubits)) + tracker = QubitTracker(num_qubits=dag.num_qubits()) + if self.data.qubits_initially_zero: + tracker.set_clean(input_qubits) + (output_circuit, output_qubits) = _run(circuit, self.data, tracker, context, input_qubits) assert isinstance(output_circuit, QuantumCircuit) out_dag = circuit_to_dag(output_circuit) @@ -355,11 +357,7 @@ def _run( """ assert isinstance(input_circuit, QuantumCircuit) - # assert context.to_globals(range(input_circuit.num_qubits)) == input_qubits - - - if input_circuit.num_qubits != context.num_qubits(): - raise TranspilerError("HighLevelSynthesis internal error.") + assert input_circuit.num_qubits == len(input_qubits) # STEP 2: Analyze the nodes in the circuit. For each node in the circuit that needs @@ -382,7 +380,6 @@ def _run( output_qubits = input_qubits num_output_qubits = len(input_qubits) - assert input_circuit.num_qubits == len(input_qubits) print(f"===> {input_circuit.num_qubits = }, {input_qubits = }") for (idx, inst) in enumerate(input_circuit): @@ -578,8 +575,8 @@ def _synthesize_operation( qubits if data.use_qubit_indices or isinstance(operation, AnnotatedOperation) else None ) if len(hls_methods := _methods_to_try(data, operation.name)) > 0: - num_clean_available = tracker.num_clean(context.to_globals(qubits)) - num_dirty_available = tracker.num_dirty(context.to_globals(qubits)) + num_clean_available = tracker.num_clean(input_qubits) + num_dirty_available = tracker.num_dirty(input_qubits) res = _synthesize_op_using_plugins( data, From 76e10a3c152d86469d5ad678fec219f3902f5c38 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 23 Dec 2024 16:23:25 +0200 Subject: [PATCH 10/32] removing context --- .../passes/synthesis/high_level_synthesis.py | 95 ++++++------------- .../passes/synthesis/hls_plugins.py | 31 +++--- 2 files changed, 39 insertions(+), 87 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index eb07c349a556..7f6d2544943d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -43,7 +43,7 @@ PowerModifier, ) -from qiskit._accelerate.high_level_synthesis import QubitTracker, QubitContext +from qiskit._accelerate.high_level_synthesis import QubitTracker from .plugin import HighLevelSynthesisPluginManager if typing.TYPE_CHECKING: @@ -317,7 +317,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: qubits = tuple(dag.find_bit(q).index for q in dag.qubits) - context = QubitContext(list(range(len(dag.qubits)))) # ToDo: try to avoid this conversion @@ -327,7 +326,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.data.qubits_initially_zero: tracker.set_clean(input_qubits) - (output_circuit, output_qubits) = _run(circuit, self.data, tracker, context, input_qubits) + (output_circuit, output_qubits) = _run(circuit, self.data, tracker, input_qubits) assert isinstance(output_circuit, QuantumCircuit) out_dag = circuit_to_dag(output_circuit) return out_dag @@ -337,7 +336,6 @@ def _run( input_circuit: QuantumCircuit, data: HLSData, tracker: QubitTracker, - context: QubitContext, input_qubits: tuple[int], ) -> tuple[QuantumCircuit, tuple[int]]: """ @@ -346,8 +344,7 @@ def _run( Input: circuit: the circuit to be synthesized. tracker: the global tracker, tracking the state of original qubits. - context: the correspondence between the circuit's qubits and the global qubits. - + The function returns the synthesized QuantumCircuit. Note that by using the auxiliary qubits to synthesize operations present in the input circuit, @@ -380,7 +377,7 @@ def _run( output_qubits = input_qubits num_output_qubits = len(input_qubits) - print(f"===> {input_circuit.num_qubits = }, {input_qubits = }") + # print(f"===> {input_circuit.num_qubits = }, {input_qubits = }") for (idx, inst) in enumerate(input_circuit): op = inst.operation @@ -388,14 +385,13 @@ def _run( # local qubit index (within the current input_circuit) op_qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) - print(f"I AM HERE 15: {input_qubits = }, {context}, {op = }, {op_qubits = }") + # print(f"I AM HERE 15: {input_qubits = }, {op = }, {op_qubits = }") # global qubits op_global_qubits = [input_qubits[q] for q in op_qubits] processed = False synthesized = None - synthesized_context = None # Start by handling special operations. Other cases can also be # considered: swaps, automatically simplifying control gate (e.g. if @@ -416,11 +412,9 @@ def _run( # next check control flow elif isinstance(op, ControlFlowOp): # print("I AM HERE") - # print(f"CONTEXT: {context}") # print(f"TRACKER: {tracker}") new_blocks = [] - inner_context = context.restrict(op_qubits) block_tracker=tracker.copy() block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_global_qubits]) @@ -430,13 +424,12 @@ def _run( # print(f"{op.num_qubits = }") # print(f"{block.num_qubits = }") # print(f"{op_global_qubits = }") - new_block = _run(block, data=data, tracker=block_tracker, context=inner_context, input_qubits=op_global_qubits)[0] + new_block = _run(block, data=data, tracker=block_tracker, input_qubits=op_global_qubits)[0] new_blocks.append(new_block) synthesized = op.replace_blocks(new_blocks) op_output_qubits = op_global_qubits # print(f"SYNTHESIZED: {synthesized}") # synthesized = _wrap_in_circuit(synthesized) - # synthesized_context = context # now we are free to synthesize @@ -445,10 +438,10 @@ def _run( # a circuit, it's the correspondence between its qubits and the global qubits). # Also note that the circuit may use auxiliary qubits. The qubits tracker and the # current circuit's context are updated in-place. - synthesized, synthesized_context, op_output_qubits = _synthesize_operation( - data, op, op_qubits, tracker, context, op_global_qubits, + synthesized, op_output_qubits = _synthesize_operation( + data, op, op_qubits, tracker, op_global_qubits, ) - print(f"{op = }, {op_output_qubits = }") + # print(f"{op = }, {op_output_qubits = }") for q in op_output_qubits: if q not in global_to_local: @@ -467,7 +460,7 @@ def _run( # We did not change anything just return the input. if len(synthesized_nodes) == 0: - print(f"I AM HERE 10: {input_circuit.num_qubits = }, {len(input_qubits) = }") + # print(f"I AM HERE 10: {input_circuit.num_qubits = }, {len(input_qubits) = }") if input_circuit.num_qubits != len(input_qubits): raise TranspilerError("HighLevelSynthesis internal error.") @@ -522,7 +515,7 @@ def _run( assert isinstance(out, QuantumCircuit) - print(f"I AM HERE: {out.num_qubits = }, {len(output_qubits) = }") + # print(f"I AM HERE: {out.num_qubits = }, {len(output_qubits) = }") if out.num_qubits != len(output_qubits): raise TranspilerError("HighLevelSynthesis internal error.") @@ -534,9 +527,8 @@ def _synthesize_operation( operation: Operation, qubits: tuple[int], tracker: QubitTracker, - context: QubitContext, input_qubits: tuple[int], -) -> tuple[QuantumCircuit | None, QubitContext | None, tuple[int]]: +) -> tuple[QuantumCircuit | None, tuple[int]]: """ 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 @@ -550,8 +542,7 @@ def _synthesize_operation( """ assert operation.num_qubits == len(input_qubits) - synthesized_context = None - print(f"OK_SO_FAR: {operation.num_qubits = }, {len(input_qubits) = }") + # print(f"OK_SO_FAR: {operation.num_qubits = }, {len(input_qubits) = }") # 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) @@ -587,33 +578,22 @@ def _synthesize_operation( num_clean_available, num_dirty_available, tracker=tracker, - context=context, ) - print(f"HERE: {operation = }, {type(res) = }, {res = }") + # print(f"HERE: {operation = }, {type(res) = }, {res = }") (synthesized, _) = res # It may happen that the plugin synthesis method uses clean/dirty ancilla qubits if synthesized is not None: - print(f"=> OK?: {synthesized.num_qubits = }, {len(qubits) = }, {len(output_qubits) = }") - if synthesized.num_qubits > len(qubits): + # print(f"=> OK?: {synthesized.num_qubits = }, {len(qubits) = }, {len(output_qubits) = }") + if synthesized.num_qubits > len(output_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() + global_aux_qubits = tracker.borrow(synthesized.num_qubits - len(output_qubits), output_qubits) + output_qubits = output_qubits + global_aux_qubits - 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 aq not in output_qubits: - output_qubits.append(aq) if synthesized is not None: - print(f"WHAT IS GOING ON: {synthesized.num_qubits = }, {len(output_qubits) = }") + # print(f"WHAT IS GOING ON: {synthesized.num_qubits = }, {len(output_qubits) = }") assert synthesized.num_qubits == len(output_qubits) # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. @@ -631,44 +611,25 @@ def _synthesize_operation( # 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. - inner_context = context.restrict(qubits) - - if synthesized.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, output_qubits = _run(synthesized, data, tracker, inner_context, output_qubits) - synthesized_context = inner_context - print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") + synthesized, output_qubits = _run(synthesized, data, tracker, output_qubits) + # print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") - if (synthesized is not None) and (synthesized.num_qubits > len(qubits)): + if (synthesized is not None) and (synthesized.num_qubits > len(output_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) + global_aux_qubits = tracker.borrow(synthesized.num_qubits - len(output_qubits), output_qubits) + output_qubits = output_qubits + global_aux_qubits - if len(qubits) > num_original_qubits: - tracker.replace_state(saved_tracker, context.to_globals(qubits[num_original_qubits:])) + if len(output_qubits) > num_original_qubits: + tracker.replace_state(saved_tracker, output_qubits[num_original_qubits:]) - - if isinstance(synthesized, DAGCircuit) and synthesized_context is None: - raise TranspilerError("HighLevelSynthesis internal error.") - assert synthesized is None or isinstance(synthesized, QuantumCircuit) - return synthesized, synthesized_context, output_qubits + return synthesized, output_qubits @@ -724,7 +685,6 @@ def _synthesize_op_using_plugins( num_clean_ancillas: int = 0, num_dirty_ancillas: int = 0, tracker: QubitTracker = None, - context: QubitContext = None, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ Attempts to synthesize op using plugin mechanism. @@ -775,7 +735,6 @@ def _synthesize_op_using_plugins( plugin_args["num_clean_ancillas"] = num_clean_ancillas plugin_args["num_dirty_ancillas"] = num_dirty_ancillas plugin_args["_qubit_tracker"] = tracker - plugin_args["_qubit_context"] = context plugin_args["_data"] = data plugin_args["input_qubits"] = input_qubits diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 37d4221dd7c5..bc6b36bfb0ee 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1652,9 +1652,9 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** operation = high_level_object modifiers = high_level_object.modifiers tracker = options.get("_qubit_tracker", None) - context = options.get("_qubit_context", None) data = options.get("_data") input_qubits = options.get("input_qubits") + output_qubits = input_qubits if len(modifiers) > 0: # Note: the base operation must be synthesized without using potential control qubits @@ -1662,20 +1662,18 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** 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 - # get qubits of base operation - control_qubits = qubits[0:num_ctrl] + baseop_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones + input_baseop_qubits = input_qubits[num_ctrl:] # Do not allow access to control qubits - tracker.disable(context.to_globals(control_qubits)) - synthesized_base_op, _, _ = _synthesize_operation( + tracker.disable(input_qubits[0:num_ctrl]) + synthesized_base_op, _ = _synthesize_operation( data, operation.base_op, baseop_qubits, tracker, - context, - input_qubits[num_ctrl:] + input_baseop_qubits ) if synthesized_base_op is None: @@ -1686,20 +1684,15 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # 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): + if synthesized_base_op.num_qubits > len(input_baseop_qubits): global_aux_qubits = tracker.borrow( - synthesized_base_op.num_qubits - len(baseop_qubits), - context.to_globals(baseop_qubits), + synthesized_base_op.num_qubits - len(input_baseop_qubits), + input_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) + output_qubits = output_qubits + global_aux_qubits + # Restore access to control qubits. - tracker.enable(context.to_globals(control_qubits)) + tracker.enable(input_qubits[0:num_ctrl]) # This step currently does not introduce ancilla qubits. synthesized = _apply_annotations(data, synthesized_base_op, operation.modifiers) From 5deb316eb235f5a35a96bd05feaa6f8fcd51294a Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 23 Dec 2024 17:33:21 +0200 Subject: [PATCH 11/32] minor --- .../passes/synthesis/high_level_synthesis.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 7f6d2544943d..c0f878e0ab2a 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -555,7 +555,7 @@ def _synthesize_operation( # 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) + num_original_qubits = len(input_qubits) qubits = list(qubits) synthesized = None @@ -566,17 +566,12 @@ def _synthesize_operation( qubits if data.use_qubit_indices or isinstance(operation, AnnotatedOperation) else None ) if len(hls_methods := _methods_to_try(data, operation.name)) > 0: - num_clean_available = tracker.num_clean(input_qubits) - num_dirty_available = tracker.num_dirty(input_qubits) - res = _synthesize_op_using_plugins( data, hls_methods, operation, indices, input_qubits, - num_clean_available, - num_dirty_available, tracker=tracker, ) @@ -681,9 +676,6 @@ def _synthesize_op_using_plugins( op: Operation, qubits: list[int] | None, input_qubits: tuple[int], - - num_clean_ancillas: int = 0, - num_dirty_ancillas: int = 0, tracker: QubitTracker = None, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ @@ -698,6 +690,9 @@ def _synthesize_op_using_plugins( an insufficient number of auxiliary qubits). """ hls_plugin_manager = data.hls_plugin_manager + num_clean_ancillas = tracker.num_clean(input_qubits) + num_dirty_ancillas = tracker.num_dirty(input_qubits) + best_decomposition = None best_score = np.inf From 663a93708169e8d04dc3400ecdbb8096d4d4c535 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Mon, 23 Dec 2024 17:52:24 +0200 Subject: [PATCH 12/32] removing unused argument --- .../passes/synthesis/high_level_synthesis.py | 16 ++++++---------- .../transpiler/passes/synthesis/hls_plugins.py | 3 +-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c0f878e0ab2a..019a9061a61d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -439,7 +439,7 @@ def _run( # Also note that the circuit may use auxiliary qubits. The qubits tracker and the # current circuit's context are updated in-place. synthesized, op_output_qubits = _synthesize_operation( - data, op, op_qubits, tracker, op_global_qubits, + data, op, tracker, op_global_qubits, ) # print(f"{op = }, {op_output_qubits = }") @@ -525,7 +525,6 @@ def _run( def _synthesize_operation( data: HLSData, operation: Operation, - qubits: tuple[int], tracker: QubitTracker, input_qubits: tuple[int], ) -> tuple[QuantumCircuit | None, tuple[int]]: @@ -556,21 +555,16 @@ def _synthesize_operation( # 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(input_qubits) - qubits = list(qubits) synthesized = None output_qubits = input_qubits # Try synthesis via HLS -- which will return ``None`` if unsuccessful. - indices = ( - qubits if data.use_qubit_indices or isinstance(operation, AnnotatedOperation) else None - ) if len(hls_methods := _methods_to_try(data, operation.name)) > 0: res = _synthesize_op_using_plugins( data, hls_methods, operation, - indices, input_qubits, tracker=tracker, ) @@ -593,7 +587,7 @@ def _synthesize_operation( # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. if synthesized is None and not data.top_level_only: - synthesized, output_qubits = _get_custom_definition(data, operation, indices, input_qubits) + synthesized, output_qubits = _get_custom_definition(data, operation, input_qubits) if synthesized is None: # if we didn't synthesize, there was nothing to unroll @@ -630,11 +624,12 @@ def _synthesize_operation( def _get_custom_definition( - data: HLSData, inst: Instruction, qubits: list[int] | None, input_qubits: tuple[int] + data: HLSData, inst: Instruction, input_qubits: tuple[int] ) -> tuple[QuantumCircuit | None, tuple[int]]: # check if the operation is already supported natively if not (isinstance(inst, ControlledGate) and inst._open_ctrl): # include path for when target exists but target.num_qubits is None (BasicSimulator) + qubits = input_qubits if data.use_qubit_indices else None inst_supported = _instruction_supported(data, inst.name, qubits) if inst_supported or ( data.equivalence_library is not None and data.equivalence_library.has_entry(inst) @@ -674,7 +669,6 @@ def _synthesize_op_using_plugins( data: HLSData, hls_methods: list, op: Operation, - qubits: list[int] | None, input_qubits: tuple[int], tracker: QubitTracker = None, ) -> tuple[QuantumCircuit | None, tuple[int]]: @@ -733,6 +727,8 @@ def _synthesize_op_using_plugins( plugin_args["_data"] = data plugin_args["input_qubits"] = input_qubits + qubits = input_qubits if data.use_qubit_indices else None + decomposition = plugin_method.run( op, coupling_map=data.coupling_map, diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index bc6b36bfb0ee..3beb4af77a01 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1663,7 +1663,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** 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_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones input_baseop_qubits = input_qubits[num_ctrl:] # Do not allow access to control qubits @@ -1671,7 +1671,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** synthesized_base_op, _ = _synthesize_operation( data, operation.base_op, - baseop_qubits, tracker, input_baseop_qubits ) From 19bdbded622ff2c51816ae084a82dd8cf139b8bc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 09:25:35 +0200 Subject: [PATCH 13/32] removing an obsolete statement --- .../passes/synthesis/high_level_synthesis.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 019a9061a61d..caa94f1e5b51 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -315,10 +315,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # The for-loop terminates without reaching the break statement return dag - - qubits = tuple(dag.find_bit(q).index for q in dag.qubits) - - # ToDo: try to avoid this conversion circuit = dag_to_circuit(dag) input_qubits = list(range(circuit.num_qubits)) @@ -326,7 +322,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.data.qubits_initially_zero: tracker.set_clean(input_qubits) - (output_circuit, output_qubits) = _run(circuit, self.data, tracker, input_qubits) + (output_circuit, _) = _run(circuit, self.data, tracker, input_qubits) assert isinstance(output_circuit, QuantumCircuit) out_dag = circuit_to_dag(output_circuit) return out_dag @@ -607,12 +603,6 @@ def _synthesize_operation( synthesized, output_qubits = _run(synthesized, data, tracker, output_qubits) # print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") - - if (synthesized is not None) and (synthesized.num_qubits > len(output_qubits)): - # need to borrow more qubits from tracker - global_aux_qubits = tracker.borrow(synthesized.num_qubits - len(output_qubits), output_qubits) - output_qubits = output_qubits + global_aux_qubits - if len(output_qubits) > num_original_qubits: tracker.replace_state(saved_tracker, output_qubits[num_original_qubits:]) From 8a73f4f96df63ff648a9323667d3d2a2f14f8538 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 09:52:53 +0200 Subject: [PATCH 14/32] minor cleanup --- .../passes/synthesis/high_level_synthesis.py | 3 ++- qiskit/transpiler/passes/synthesis/hls_plugins.py | 12 +----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index caa94f1e5b51..341429396119 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -660,7 +660,7 @@ def _synthesize_op_using_plugins( hls_methods: list, op: Operation, input_qubits: tuple[int], - tracker: QubitTracker = None, + tracker: QubitTracker, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ Attempts to synthesize op using plugin mechanism. @@ -673,6 +673,7 @@ def _synthesize_op_using_plugins( when no synthesis methods is available or specified, or when there is an insufficient number of auxiliary qubits). """ + hls_plugin_manager = data.hls_plugin_manager num_clean_ancillas = tracker.num_clean(input_qubits) num_dirty_ancillas = tracker.num_dirty(input_qubits) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 3beb4af77a01..f4b56f7fccba 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1668,7 +1668,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # Do not allow access to control qubits tracker.disable(input_qubits[0:num_ctrl]) - synthesized_base_op, _ = _synthesize_operation( + synthesized_base_op, output_qubits = _synthesize_operation( data, operation.base_op, tracker, @@ -1680,16 +1680,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** assert not isinstance(synthesized_base_op, DAGCircuit) - # 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(input_baseop_qubits): - global_aux_qubits = tracker.borrow( - synthesized_base_op.num_qubits - len(input_baseop_qubits), - input_baseop_qubits, - ) - output_qubits = output_qubits + global_aux_qubits - # Restore access to control qubits. tracker.enable(input_qubits[0:num_ctrl]) From 0a6950b33648297340ba299d8ebbd9dad20e8159 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 10:08:07 +0200 Subject: [PATCH 15/32] minor --- .../passes/synthesis/high_level_synthesis.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 341429396119..e394adfbe673 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -557,7 +557,7 @@ def _synthesize_operation( # Try synthesis via HLS -- which will return ``None`` if unsuccessful. if len(hls_methods := _methods_to_try(data, operation.name)) > 0: - res = _synthesize_op_using_plugins( + (synthesized, output_qubits) = _synthesize_op_using_plugins( data, hls_methods, operation, @@ -565,18 +565,6 @@ def _synthesize_operation( tracker=tracker, ) - # print(f"HERE: {operation = }, {type(res) = }, {res = }") - (synthesized, _) = res - - # It may happen that the plugin synthesis method uses clean/dirty ancilla qubits - if synthesized is not None: - # print(f"=> OK?: {synthesized.num_qubits = }, {len(qubits) = }, {len(output_qubits) = }") - if synthesized.num_qubits > len(output_qubits): - # need to borrow more qubits from tracker - global_aux_qubits = tracker.borrow(synthesized.num_qubits - len(output_qubits), output_qubits) - output_qubits = output_qubits + global_aux_qubits - - if synthesized is not None: # print(f"WHAT IS GOING ON: {synthesized.num_qubits = }, {len(output_qubits) = }") assert synthesized.num_qubits == len(output_qubits) @@ -673,7 +661,7 @@ def _synthesize_op_using_plugins( when no synthesis methods is available or specified, or when there is an insufficient number of auxiliary qubits). """ - + hls_plugin_manager = data.hls_plugin_manager num_clean_ancillas = tracker.num_clean(input_qubits) num_dirty_ancillas = tracker.num_dirty(input_qubits) @@ -744,8 +732,14 @@ def _synthesize_op_using_plugins( best_decomposition = decomposition best_score = current_score - # FIXME: - return (best_decomposition, None) + + output_qubits = input_qubits + if best_decomposition is not None: + if best_decomposition.num_qubits > len(input_qubits): + global_aux_qubits = tracker.borrow(best_decomposition.num_qubits - len(output_qubits), output_qubits) + output_qubits = output_qubits + global_aux_qubits + + return (best_decomposition, output_qubits) def _definitely_skip_node( From bbc217579ac7ebce64905c1a6b73b6352bf51f5f Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 15:03:00 +0200 Subject: [PATCH 16/32] improvements to run method --- .../passes/synthesis/high_level_synthesis.py | 213 +++++++----------- 1 file changed, 83 insertions(+), 130 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index e394adfbe673..790b0de1529b 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -337,6 +337,8 @@ def _run( """ The main recursive function that synthesizes a QuantumCircuit. + FIXME!!! + Input: circuit: the circuit to be synthesized. tracker: the global tracker, tracking the state of original qubits. @@ -352,7 +354,8 @@ def _run( assert isinstance(input_circuit, QuantumCircuit) assert input_circuit.num_qubits == len(input_qubits) - + + # FIXME!!! # STEP 2: Analyze the nodes in the circuit. For each node in the circuit that needs # to be synthesized, we recursively synthesize it and store the result. For # instance, the result of synthesizing a custom gate is a QuantumCircuit corresponding @@ -364,158 +367,108 @@ def _run( # 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 circuit. - synthesized_nodes = {} + # STEP 3. We rebuild the circuit 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. + + output_circuit = input_circuit.copy_empty_like() + output_qubits = input_qubits global_to_local = dict() for i, q in enumerate(input_qubits): global_to_local[q] = i - output_qubits = input_qubits num_output_qubits = len(input_qubits) - # print(f"===> {input_circuit.num_qubits = }, {input_qubits = }") - - for (idx, inst) in enumerate(input_circuit): + for inst in input_circuit: op = inst.operation - # local qubit index (within the current input_circuit) - op_qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) - - # print(f"I AM HERE 15: {input_qubits = }, {op = }, {op_qubits = }") + # FIXME: combine the following two lines into one - # global qubits - op_global_qubits = [input_qubits[q] for q in op_qubits] + # op's qubits as viewed from within the input circuit + op_local_qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) - processed = False - synthesized = None + # op's qubits as viewed globally + op_qubits = [input_qubits[q] for q in op_local_qubits] - # Start by handling special operations. Other cases can also be - # considered: swaps, automatically simplifying control gate (e.g. if - # a control is 0). + # Start by handling special operations. + # In the future, we can also consider other possible optimizations, e.g.: + # - improved qubit tracking after a SWAP gate + # - automatically simplify control gates with control at 0. if op.name in ["id", "delay", "barrier"]: - # tracker not updated, these are no-ops - processed = True - - elif op.name == "reset": - # reset qubits to 0 - tracker.set_clean(op_global_qubits) - processed = True - - # check if synthesis for the operation can be skipped - elif _definitely_skip_op(data, op, op_global_qubits, input_circuit): - tracker.set_dirty(op_global_qubits) - - # next check control flow - elif isinstance(op, ControlFlowOp): - # print("I AM HERE") - # print(f"TRACKER: {tracker}") - + # tracker is not updated, these are no-ops + output_circuit.append(op, inst.qubits, inst.clbits) + continue + + if op.name == "reset": + # tracker resets qubits to 0 + output_circuit.append(op, inst.qubits, inst.clbits) + tracker.set_clean(op_qubits) + continue + + # Check if synthesis for this operation can be skipped + if _definitely_skip_op(data, op, op_qubits, input_circuit): + output_circuit.append(op, inst.qubits, inst.clbits) + tracker.set_dirty(op_qubits) + continue + + # Recursively handle control-flow + if isinstance(op, ControlFlowOp): new_blocks = [] block_tracker=tracker.copy() - block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_global_qubits]) + block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_qubits]) for block in op.blocks: - # print("=================") - # print(block) - # print(f"{op.num_qubits = }") - # print(f"{block.num_qubits = }") - # print(f"{op_global_qubits = }") - new_block = _run(block, data=data, tracker=block_tracker, input_qubits=op_global_qubits)[0] + new_block = _run(block, data=data, tracker=block_tracker, input_qubits=op_qubits)[0] new_blocks.append(new_block) - synthesized = op.replace_blocks(new_blocks) - op_output_qubits = op_global_qubits - # print(f"SYNTHESIZED: {synthesized}") - # synthesized = _wrap_in_circuit(synthesized) - - - # now we are free to synthesize - else: - # This returns the synthesized operation and its context (when the result is - # a circuit, it's the correspondence between its qubits and the global qubits). - # Also note that the circuit may use auxiliary qubits. The qubits tracker and the - # current circuit's context are updated in-place. - synthesized, op_output_qubits = _synthesize_operation( - data, op, tracker, op_global_qubits, - ) - # print(f"{op = }, {op_output_qubits = }") - - for q in op_output_qubits: - if q not in global_to_local: - global_to_local[q] = num_output_qubits - num_output_qubits += 1 - output_qubits.append(q) - - - # If the synthesis changed the operation (i.e. it is not None), store the result. - if synthesized is not None: - synthesized_nodes[idx] = (synthesized, op_output_qubits) + synthesized_op = op.replace_blocks(new_blocks) + output_circuit.append(synthesized_op, inst.qubits, inst.clbits) + synthesized_op_qubits = op_qubits + tracker.set_dirty(synthesized_op_qubits) + continue + + # The function synthesize_operations returns None if the operation does not need to be + # synthesized, or a quantum circuit together with the global qubits on which this + # circuit is defined. Also note that the synthesized circuit may involve auxiliary + # global qubits not used by the input circuit. + synthesized_circuit, synthesized_circuit_qubits = _synthesize_operation(data, op, tracker, op_qubits) # If the synthesis did not change anything, just update the qubit tracker. - elif not processed: - tracker.set_dirty(op_global_qubits) - - # We did not change anything just return the input. - if len(synthesized_nodes) == 0: - # print(f"I AM HERE 10: {input_circuit.num_qubits = }, {len(input_qubits) = }") - if input_circuit.num_qubits != len(input_qubits): - raise TranspilerError("HighLevelSynthesis internal error.") - - assert isinstance(input_circuit, QuantumCircuit) - return (input_circuit, input_qubits) - - # STEP 3. We rebuild the circuit 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 = input_circuit.copy_empty_like() - num_additional_qubits = num_output_qubits - out.num_qubits - - if num_additional_qubits > 0: - out.add_bits([Qubit() for _ in range(num_additional_qubits)]) - - index_to_qubit = dict(enumerate(out.qubits)) - - for (idx, inst) in enumerate(input_circuit): - op = inst.operation - - if op_tuple := synthesized_nodes.get(idx, None): - op, op_output_qubits = op_tuple - - if isinstance(op, Operation): - # We are here for controlled flow ops - # We should not be here - # assert False - # out.apply_operation_back(op, node.qargs, node.cargs) - # print(f"{inst.qubits = }, {inst.clbits = }") - out.append(op, inst.qubits, inst.clbits) - continue - - assert isinstance(op, QuantumCircuit) - - qubit_map = { - q: index_to_qubit[global_to_local[op_output_qubits[i]]] - for (i, q) in enumerate(op.qubits) - } - clbit_map = dict(zip(op.clbits, inst.clbits)) - - for sub_node in op: - out.append( - sub_node.operation, - tuple(qubit_map[qarg] for qarg in sub_node.qubits), - tuple(clbit_map[carg] for carg in sub_node.clbits), - ) - out.global_phase += op.global_phase - - - else: - out.append(op, inst.qubits, inst.clbits) + if synthesized_circuit is None: + output_circuit.append(op, inst.qubits, inst.clbits) + tracker.set_dirty(op_qubits) + continue + + if not isinstance(synthesized_circuit, QuantumCircuit): + raise TranspilerError("HighLevelSynthesis error: synthesize_operation did not return a QuantumCircuit") + + # FIXME: see if this can be improved + + # Update output circuit's qubits + for q in synthesized_circuit_qubits: + if q not in global_to_local: + global_to_local[q] = num_output_qubits + num_output_qubits += 1 + output_qubits.append(q) + output_circuit.add_bits([Qubit()]) + + # Add the operations from the op's synthesized circuit to the output circuit, using the correspondence + # syntesized circuit's qubits -> global qubits -> output circuit's qubits + qubit_map = {synthesized_circuit.qubits[i]: output_circuit.qubits[global_to_local[q]] for (i, q) in enumerate(synthesized_circuit_qubits)} + clbit_map = dict(zip(synthesized_circuit.clbits, output_circuit.clbits)) + + for inst_inner in synthesized_circuit: + output_circuit.append(inst_inner.operation, + tuple(qubit_map[q] for q in inst_inner.qubits), + tuple(clbit_map[c] for c in inst_inner.clbits), + ) + output_circuit.global_phase += synthesized_circuit.global_phase - assert isinstance(out, QuantumCircuit) - # print(f"I AM HERE: {out.num_qubits = }, {len(output_qubits) = }") - if out.num_qubits != len(output_qubits): - raise TranspilerError("HighLevelSynthesis internal error.") + if output_circuit.num_qubits != len(output_qubits): + raise TranspilerError("HighLevelSynthesis error: ") - return (out, output_qubits) + return (output_circuit, output_qubits) def _synthesize_operation( From a02ed031301ddf590041f1e3340cf97913eccbc4 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 16:03:55 +0200 Subject: [PATCH 17/32] cleanup --- .../passes/synthesis/high_level_synthesis.py | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 790b0de1529b..3179575dc47d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -322,7 +322,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.data.qubits_initially_zero: tracker.set_clean(input_qubits) - (output_circuit, _) = _run(circuit, self.data, tracker, input_qubits) + (output_circuit, _) = _run(circuit, input_qubits, self.data, tracker) assert isinstance(output_circuit, QuantumCircuit) out_dag = circuit_to_dag(output_circuit) return out_dag @@ -330,46 +330,42 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: def _run( input_circuit: QuantumCircuit, + input_qubits: tuple[int], data: HLSData, tracker: QubitTracker, - input_qubits: tuple[int], ) -> tuple[QuantumCircuit, tuple[int]]: """ - The main recursive function that synthesizes a QuantumCircuit. + Recursively synthesizes a subcircuit. This subcircuit may be either the original + circuit, the definition circuit for one of the gates, or a circuit returned by + by a plugin. - FIXME!!! - Input: - circuit: the circuit to be synthesized. - tracker: the global tracker, tracking the state of original qubits. + input_circuit: the subcircuit to be synthesized. + input_qubits: a list of global qubits (qubits in the original circuit) over + which the input circuit is defined. + data: high-level-synthesis data and options. + tracker: the global tracker, tracking the state of global qubits. - The function returns the synthesized QuantumCircuit. - - Note that by using the auxiliary qubits to synthesize operations present in the input circuit, - the synthesized circuit may be defined over more qubits than the input circuit. In this case, - the function update in-place the global qubits tracker and extends the local-to-global - context. + The function returns the synthesized circuit and the global qubits over which this + output circuit is defined. Note that by using the auxiliary qubits, the output circuit + may be defined over more qubits than the input circuit. + + The function also updates in-place the qubit tracker which keeps track of the status of + each global qubit (whether it's clean, dirty, or cannot be used). """ - assert isinstance(input_circuit, QuantumCircuit) - assert input_circuit.num_qubits == len(input_qubits) - + if not isinstance(input_circuit, QuantumCircuit) or (input_circuit.num_qubits != len(input_qubits)): + raise TranspilerError("HighLevelSynthesis error: the input to 'run' is incorrect.") - # FIXME!!! - # STEP 2: Analyze the nodes in the circuit. For each node in the circuit that needs - # to be synthesized, we recursively synthesize it and store the result. For - # instance, the result of synthesizing a custom gate is a QuantumCircuit corresponding - # to the (recursively synthesized) gate's definition. When the result is a - # circuit, 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. + # We iteratively process circuit instructions in the order they appear in the input circuit, + # and add the synthesized instructions to the output circuit. Note that in the process the + # output circuit may need to be extended with additional qubits. In addition, we keep track + # of the status of the original qubits using the 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 circuit. - # STEP 3. We rebuild the circuit 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. output_circuit = input_circuit.copy_empty_like() output_qubits = input_qubits @@ -419,7 +415,7 @@ def _run( block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_qubits]) for block in op.blocks: - new_block = _run(block, data=data, tracker=block_tracker, input_qubits=op_qubits)[0] + new_block = _run(block, input_qubits=op_qubits, data=data, tracker=block_tracker)[0] new_blocks.append(new_block) synthesized_op = op.replace_blocks(new_blocks) output_circuit.append(synthesized_op, inst.qubits, inst.clbits) @@ -541,7 +537,7 @@ def _synthesize_operation( # 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, output_qubits = _run(synthesized, data, tracker, output_qubits) + synthesized, output_qubits = _run(synthesized, output_qubits, data, tracker) # print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") if len(output_qubits) > num_original_qubits: From 53c7dee5d8676ecc7df2088501372acf87d24ed4 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 16:41:27 +0200 Subject: [PATCH 18/32] another pass over the run method + black --- .../passes/synthesis/high_level_synthesis.py | 113 ++++++++++-------- .../passes/synthesis/hls_plugins.py | 5 +- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 3179575dc47d..b97b7b685115 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -335,26 +335,28 @@ def _run( tracker: QubitTracker, ) -> tuple[QuantumCircuit, tuple[int]]: """ - Recursively synthesizes a subcircuit. This subcircuit may be either the original + Recursively synthesizes a subcircuit. This subcircuit may be either the original circuit, the definition circuit for one of the gates, or a circuit returned by - by a plugin. + by a plugin. Input: input_circuit: the subcircuit to be synthesized. - input_qubits: a list of global qubits (qubits in the original circuit) over + input_qubits: a list of global qubits (qubits in the original circuit) over which the input circuit is defined. data: high-level-synthesis data and options. tracker: the global tracker, tracking the state of global qubits. - - The function returns the synthesized circuit and the global qubits over which this - output circuit is defined. Note that by using the auxiliary qubits, the output circuit - may be defined over more qubits than the input circuit. - + + The function returns the synthesized circuit and the global qubits over which this + output circuit is defined. Note that by using the auxiliary qubits, the output circuit + may be defined over more qubits than the input circuit. + The function also updates in-place the qubit tracker which keeps track of the status of each global qubit (whether it's clean, dirty, or cannot be used). """ - if not isinstance(input_circuit, QuantumCircuit) or (input_circuit.num_qubits != len(input_qubits)): + if not isinstance(input_circuit, QuantumCircuit) or ( + input_circuit.num_qubits != len(input_qubits) + ): raise TranspilerError("HighLevelSynthesis error: the input to 'run' is incorrect.") # We iteratively process circuit instructions in the order they appear in the input circuit, @@ -370,34 +372,26 @@ def _run( output_circuit = input_circuit.copy_empty_like() output_qubits = input_qubits - global_to_local = dict() - for i, q in enumerate(input_qubits): - global_to_local[q] = i - - num_output_qubits = len(input_qubits) + # The "inverse" map from the global qubits to the output circuit's qubits. + # This map may be extended if additional auxiliary qubits get used. + global_to_local = {q: i for i, q in enumerate(output_qubits)} for inst in input_circuit: op = inst.operation - # FIXME: combine the following two lines into one - - # op's qubits as viewed from within the input circuit - op_local_qubits = tuple(input_circuit.find_bit(q).index for q in inst.qubits) - # op's qubits as viewed globally - op_qubits = [input_qubits[q] for q in op_local_qubits] + op_qubits = [input_qubits[input_circuit.find_bit(q).index] for q in inst.qubits] - # Start by handling special operations. + # Start by handling special operations. # In the future, we can also consider other possible optimizations, e.g.: # - improved qubit tracking after a SWAP gate - # - automatically simplify control gates with control at 0. + # - automatically simplify control gates with control at 0. if op.name in ["id", "delay", "barrier"]: - # tracker is not updated, these are no-ops output_circuit.append(op, inst.qubits, inst.clbits) + # tracker is not updated, these are no-ops continue if op.name == "reset": - # tracker resets qubits to 0 output_circuit.append(op, inst.qubits, inst.clbits) tracker.set_clean(op_qubits) continue @@ -408,61 +402,79 @@ def _run( tracker.set_dirty(op_qubits) continue - # Recursively handle control-flow + # Recursively handle control-flow. + # Currently we do not allow subcircuits within the control flow to use auxiliary qubits + # and mark all the usable qubits as dirty. This is done in order to avoid complications + # that different subcircuits may choose to use different auxiliary global qubits, and to + # avoid complications related to tracking qubit status for while- loops. + # In the future, this handling can potentially be improved. if isinstance(op, ControlFlowOp): new_blocks = [] - block_tracker=tracker.copy() + block_tracker = tracker.copy() block_tracker.disable([q for q in range(tracker.num_qubits()) if q not in op_qubits]) - + block_tracker.set_dirty(op_qubits) for block in op.blocks: new_block = _run(block, input_qubits=op_qubits, data=data, tracker=block_tracker)[0] new_blocks.append(new_block) synthesized_op = op.replace_blocks(new_blocks) + # The block circuits are defined over the same qubits and clbits as the original + # instruction. output_circuit.append(synthesized_op, inst.qubits, inst.clbits) - synthesized_op_qubits = op_qubits - tracker.set_dirty(synthesized_op_qubits) + tracker.set_dirty(op_qubits) continue - # The function synthesize_operations returns None if the operation does not need to be + # Now we synthesize the operation. + # The function synthesize_operation returns None if the operation does not need to be # synthesized, or a quantum circuit together with the global qubits on which this # circuit is defined. Also note that the synthesized circuit may involve auxiliary # global qubits not used by the input circuit. - synthesized_circuit, synthesized_circuit_qubits = _synthesize_operation(data, op, tracker, op_qubits) + synthesized_circuit, synthesized_circuit_qubits = _synthesize_operation( + data, op, tracker, op_qubits + ) - # If the synthesis did not change anything, just update the qubit tracker. + # If the synthesis did not change anything, we add the operation to the output circuit and update the + # qubit tracker. if synthesized_circuit is None: output_circuit.append(op, inst.qubits, inst.clbits) tracker.set_dirty(op_qubits) continue - if not isinstance(synthesized_circuit, QuantumCircuit): - raise TranspilerError("HighLevelSynthesis error: synthesize_operation did not return a QuantumCircuit") - - # FIXME: see if this can be improved + # This pedantic check can possibly be removed. + if not isinstance(synthesized_circuit, QuantumCircuit) or ( + synthesized_circuit.num_qubits != len(synthesized_circuit_qubits) + ): + raise TranspilerError( + "HighLevelSynthesis error: the output from 'synthesize_operation' is incorrect." + ) - # Update output circuit's qubits + # If the synthesized circuit uses (auxiliary) global qubits that are not in the output circuit, + # we add these qubits to the output circuit. for q in synthesized_circuit_qubits: if q not in global_to_local: - global_to_local[q] = num_output_qubits - num_output_qubits += 1 + global_to_local[q] = len(output_qubits) output_qubits.append(q) output_circuit.add_bits([Qubit()]) - # Add the operations from the op's synthesized circuit to the output circuit, using the correspondence + # Add the operations from the synthesized circuit to the output circuit, using the correspondence # syntesized circuit's qubits -> global qubits -> output circuit's qubits - qubit_map = {synthesized_circuit.qubits[i]: output_circuit.qubits[global_to_local[q]] for (i, q) in enumerate(synthesized_circuit_qubits)} + qubit_map = { + synthesized_circuit.qubits[i]: output_circuit.qubits[global_to_local[q]] + for (i, q) in enumerate(synthesized_circuit_qubits) + } clbit_map = dict(zip(synthesized_circuit.clbits, output_circuit.clbits)) for inst_inner in synthesized_circuit: - output_circuit.append(inst_inner.operation, + output_circuit.append( + inst_inner.operation, tuple(qubit_map[q] for q in inst_inner.qubits), tuple(clbit_map[c] for c in inst_inner.clbits), ) output_circuit.global_phase += synthesized_circuit.global_phase + # Another pedantic check that can possibly be removed. if output_circuit.num_qubits != len(output_qubits): - raise TranspilerError("HighLevelSynthesis error: ") + raise TranspilerError("HighLevelSynthesis error: the input from 'run' is incorrect.") return (output_circuit, output_qubits) @@ -543,13 +555,10 @@ def _synthesize_operation( if len(output_qubits) > num_original_qubits: tracker.replace_state(saved_tracker, output_qubits[num_original_qubits:]) - assert synthesized is None or isinstance(synthesized, QuantumCircuit) return synthesized, output_qubits - - def _get_custom_definition( data: HLSData, inst: Instruction, input_qubits: tuple[int] ) -> tuple[QuantumCircuit | None, tuple[int]]: @@ -615,7 +624,6 @@ def _synthesize_op_using_plugins( num_clean_ancillas = tracker.num_clean(input_qubits) num_dirty_ancillas = tracker.num_dirty(input_qubits) - best_decomposition = None best_score = np.inf @@ -681,11 +689,12 @@ def _synthesize_op_using_plugins( best_decomposition = decomposition best_score = current_score - output_qubits = input_qubits if best_decomposition is not None: if best_decomposition.num_qubits > len(input_qubits): - global_aux_qubits = tracker.borrow(best_decomposition.num_qubits - len(output_qubits), output_qubits) + global_aux_qubits = tracker.borrow( + best_decomposition.num_qubits - len(output_qubits), output_qubits + ) output_qubits = output_qubits + global_aux_qubits return (best_decomposition, output_qubits) @@ -734,9 +743,7 @@ def _definitely_skip_node( # ToDo: try to avoid duplication with other function -def _definitely_skip_op( - data: HLSData, op: Operation, qubits: tuple[int], dag: DAGCircuit -) -> bool: +def _definitely_skip_op(data: HLSData, op: Operation, qubits: tuple[int], dag: DAGCircuit) -> bool: """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will attempt to synthesise it) without accessing its Python-space `Operation`. @@ -787,4 +794,4 @@ def _instruction_supported(data: HLSData, name: str, qubits: tuple[int] | None) def _wrap_in_circuit(op: Operation) -> QuantumCircuit: circuit = QuantumCircuit(op.num_qubits, op.num_clbits) circuit.append(op, circuit.qubits, circuit.clbits) - return circuit \ No newline at end of file + return circuit diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index f4b56f7fccba..3ed3922c9735 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1669,10 +1669,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # Do not allow access to control qubits tracker.disable(input_qubits[0:num_ctrl]) synthesized_base_op, output_qubits = _synthesize_operation( - data, - operation.base_op, - tracker, - input_baseop_qubits + data, operation.base_op, tracker, input_baseop_qubits ) if synthesized_base_op is None: From 23d2ef71b81fe2dfc7d7c4d7e0db6556bac3d85c Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 19:30:39 +0200 Subject: [PATCH 19/32] minor cleanup --- .../passes/synthesis/high_level_synthesis.py | 59 +++++++++++-------- .../passes/synthesis/hls_plugins.py | 2 +- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index b97b7b685115..64fb6a455fe8 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -347,7 +347,7 @@ def _run( tracker: the global tracker, tracking the state of global qubits. The function returns the synthesized circuit and the global qubits over which this - output circuit is defined. Note that by using the auxiliary qubits, the output circuit + output circuit is defined. Note that by using auxiliary qubits, the output circuit may be defined over more qubits than the input circuit. The function also updates in-place the qubit tracker which keeps track of the status of @@ -429,7 +429,7 @@ def _run( # circuit is defined. Also note that the synthesized circuit may involve auxiliary # global qubits not used by the input circuit. synthesized_circuit, synthesized_circuit_qubits = _synthesize_operation( - data, op, tracker, op_qubits + op, op_qubits, data, tracker ) # If the synthesis did not change anything, we add the operation to the output circuit and update the @@ -480,37 +480,44 @@ def _run( def _synthesize_operation( - data: HLSData, operation: Operation, - tracker: QubitTracker, input_qubits: tuple[int], + data: HLSData, + tracker: QubitTracker, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ - 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). + Recursively synthesizes a single operation. + + Input: + operation: the operation to be synthesized. + input_qubits: a list of global qubits (qubits in the original circuit) over + which the operation is defined. + data: high-level-synthesis data and options. + tracker: the global tracker, tracking the state of global qubits. + + The function returns the synthesized circuit and the global qubits over which this + output circuit is defined. Note that by using auxiliary qubits, the output circuit + may be defined over more qubits than the input operation. In addition, the output + circuit may be ``None``, which means that the operation should remain as it is. + + The function also updates in-place the qubit tracker which keeps track of the status of + each global qubit (whether it's clean, dirty, or cannot be used). """ - assert operation.num_qubits == len(input_qubits) - # print(f"OK_SO_FAR: {operation.num_qubits = }, {len(input_qubits) = }") - # 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) - # or operation (e.g adding control on an X gate). - # (2) High-level objects: try running the battery of high-level synthesis plugins (e.g. - # 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 operation.num_qubits != len(input_qubits): + raise TranspilerError("HighLevelSynthesis error: the input to 'synthesize_operation' is incorrect.") + + # Synthesize the operation: + # + # (1) Synthesis plugins: try running the battery of high-level synthesis plugins (e.g. + # if the operation is a Clifford). If succeeds, this returns a circuit. The plugin + # mechanism also includes handling of AnnonatedOperations. + # (2) Unrolling custom definitions: try defining the operation if it is not yet + # in the set of supported instructions. If succeeds, this 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``. + # If any of the above is triggered, the returned circuit is recursively synthesized, + # so that the final circuit only consists of supported operations. If there was no change, + # we just return ``None``. num_original_qubits = len(input_qubits) synthesized = None diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 3ed3922c9735..c7decf0ba2ef 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1669,7 +1669,7 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** # Do not allow access to control qubits tracker.disable(input_qubits[0:num_ctrl]) synthesized_base_op, output_qubits = _synthesize_operation( - data, operation.base_op, tracker, input_baseop_qubits + operation.base_op, input_baseop_qubits, data, tracker ) if synthesized_base_op is None: From 3f2904b5ab649f9b2aa72af134f9495577da1672 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 19:44:38 +0200 Subject: [PATCH 20/32] pass over synthesize_operation --- .../passes/synthesis/high_level_synthesis.py | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 64fb6a455fe8..9e9a1747d645 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -486,7 +486,7 @@ def _synthesize_operation( tracker: QubitTracker, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ - Recursively synthesizes a single operation. + Recursively synthesizes a single operation. Input: operation: the operation to be synthesized. @@ -494,76 +494,79 @@ def _synthesize_operation( which the operation is defined. data: high-level-synthesis data and options. tracker: the global tracker, tracking the state of global qubits. - + The function returns the synthesized circuit and the global qubits over which this output circuit is defined. Note that by using auxiliary qubits, the output circuit may be defined over more qubits than the input operation. In addition, the output - circuit may be ``None``, which means that the operation should remain as it is. + circuit may be ``None``, which means that the operation should remain as it is. The function also updates in-place the qubit tracker which keeps track of the status of each global qubit (whether it's clean, dirty, or cannot be used). """ if operation.num_qubits != len(input_qubits): - raise TranspilerError("HighLevelSynthesis error: the input to 'synthesize_operation' is incorrect.") + raise TranspilerError( + "HighLevelSynthesis error: the input to 'synthesize_operation' is incorrect." + ) - # Synthesize the operation: + # Synthesize the operation: # - # (1) Synthesis plugins: try running the battery of high-level synthesis plugins (e.g. - # if the operation is a Clifford). If succeeds, this returns a circuit. The plugin - # mechanism also includes handling of AnnonatedOperations. + # (1) Synthesis plugins: try running the battery of high-level synthesis plugins (e.g. + # if the operation is a Clifford). If succeeds, this returns a circuit. The plugin + # mechanism also includes handling of AnnonatedOperations. # (2) Unrolling custom definitions: try defining the operation if it is not yet # in the set of supported instructions. If succeeds, this returns a circuit. # # If any of the above is triggered, the returned circuit is recursively synthesized, - # so that the final circuit only consists of supported operations. If there was no change, + # so that the final circuit only consists of supported operations. If there was no change, # we just return ``None``. num_original_qubits = len(input_qubits) - synthesized = None + output_circuit = None output_qubits = input_qubits # Try synthesis via HLS -- which will return ``None`` if unsuccessful. if len(hls_methods := _methods_to_try(data, operation.name)) > 0: - (synthesized, output_qubits) = _synthesize_op_using_plugins( - data, - hls_methods, + output_circuit, output_qubits = _synthesize_op_using_plugins( operation, input_qubits, - tracker=tracker, + data, + tracker, + hls_methods, ) - if synthesized is not None: - # print(f"WHAT IS GOING ON: {synthesized.num_qubits = }, {len(output_qubits) = }") - assert synthesized.num_qubits == len(output_qubits) - # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. - if synthesized is None and not data.top_level_only: - synthesized, output_qubits = _get_custom_definition(data, operation, input_qubits) + if output_circuit is None and not data.top_level_only: + output_circuit, output_qubits = _get_custom_definition(data, operation, input_qubits) - if synthesized is None: - # if we didn't synthesize, there was nothing to unroll - # updating the tracker will be handled upstream + if output_circuit is not None: + if not isinstance(output_circuit, QuantumCircuit) or ( + output_circuit.num_qubits != len(output_qubits) + ): + raise TranspilerError( + "HighLevelSynthesis error: the intermediate circuit is incorrect." + ) + + if output_circuit is None: + # if we didn't synthesize, there is nothing to do. + # Updating the tracker will be handled upstream. pass else: - assert 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. - + # Output circuit is a quantum circuit which we want to process recursively. # 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. + # qubits to the current positions. saved_tracker = tracker.copy() - synthesized, output_qubits = _run(synthesized, output_qubits, data, tracker) - # print(f"CHECK: {synthesized.num_qubits = }, {output_qubits = }, {len(output_qubits) = }") + output_circuit, output_qubits = _run(output_circuit, output_qubits, data, tracker) if len(output_qubits) > num_original_qubits: tracker.replace_state(saved_tracker, output_qubits[num_original_qubits:]) - assert synthesized is None or isinstance(synthesized, QuantumCircuit) - return synthesized, output_qubits + if (output_circuit is not None) and (output_circuit.num_qubits != len(output_qubits)): + raise TranspilerError( + "HighLevelSynthesis error: the output of 'synthesize_operation' is incorrect." + ) + + return output_circuit, output_qubits def _get_custom_definition( @@ -609,11 +612,7 @@ def _methods_to_try(data: HLSData, name: str): def _synthesize_op_using_plugins( - data: HLSData, - hls_methods: list, - op: Operation, - input_qubits: tuple[int], - tracker: QubitTracker, + op: Operation, input_qubits: tuple[int], data: HLSData, tracker: QubitTracker, hls_methods: list ) -> tuple[QuantumCircuit | None, tuple[int]]: """ Attempts to synthesize op using plugin mechanism. From 09f5c15195f0e0e82d624514cc174bf119a248e1 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 20:20:58 +0200 Subject: [PATCH 21/32] more cleanup --- .../passes/synthesis/high_level_synthesis.py | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 9e9a1747d645..f0d843c4bd01 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -537,7 +537,7 @@ def _synthesize_operation( # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. if output_circuit is None and not data.top_level_only: - output_circuit, output_qubits = _get_custom_definition(data, operation, input_qubits) + output_circuit, output_qubits = _get_custom_definition(operation, input_qubits, data) if output_circuit is not None: if not isinstance(output_circuit, QuantumCircuit) or ( @@ -570,26 +570,32 @@ def _synthesize_operation( def _get_custom_definition( - data: HLSData, inst: Instruction, input_qubits: tuple[int] + operation: Operation, input_qubits: tuple[int], data: HLSData ) -> tuple[QuantumCircuit | None, tuple[int]]: + """Returns the definition for the given operation. + + Returns None if the operation is already supported or does not have + the definition. + """ + # check if the operation is already supported natively - if not (isinstance(inst, ControlledGate) and inst._open_ctrl): + if not (isinstance(operation, ControlledGate) and operation._open_ctrl): # include path for when target exists but target.num_qubits is None (BasicSimulator) qubits = input_qubits if data.use_qubit_indices else None - inst_supported = _instruction_supported(data, inst.name, qubits) + inst_supported = _instruction_supported(data, operation.name, qubits) if inst_supported or ( - data.equivalence_library is not None and data.equivalence_library.has_entry(inst) + data.equivalence_library is not None and data.equivalence_library.has_entry(operation) ): return (None, input_qubits) # we support this operation already # if not, try to get the definition try: - definition = inst.definition + definition = operation.definition except (TypeError, AttributeError) as err: - raise TranspilerError(f"HighLevelSynthesis was unable to define {inst.name}.") from err + raise TranspilerError(f"HighLevelSynthesis is unable to define {operation.name}.") from err if definition is None: - raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {inst}.") + raise TranspilerError(f"HighLevelSynthesis is unable to synthesize {operation}.") return (definition, input_qubits) @@ -612,16 +618,20 @@ def _methods_to_try(data: HLSData, name: str): def _synthesize_op_using_plugins( - op: Operation, input_qubits: tuple[int], data: HLSData, tracker: QubitTracker, hls_methods: list + operation: Operation, input_qubits: tuple[int], data: HLSData, tracker: QubitTracker, hls_methods: list ) -> tuple[QuantumCircuit | None, tuple[int]]: """ - Attempts to synthesize op using plugin mechanism. + Attempts to synthesize an operation using plugin mechanism. - The arguments ``num_clean_ancillas`` and ``num_dirty_ancillas`` specify - the number of clean and dirty qubits available to synthesize the given - operation. A synthesis method does not need to use these additional qubits. + Input: + operation: the operation to be synthesized. + input_qubits: a list of global qubits (qubits in the original circuit) over + which the operation is defined. + data: high-level-synthesis data and options. + tracker: the global tracker, tracking the state of global qubits. + hls_methods: the list of synthesis methods to try. - Returns either the synthesized circuit or None (which may occur + Returns either the synthesized circuit or ``None`` (which may occur when no synthesis methods is available or specified, or when there is an insufficient number of auxiliary qubits). """ @@ -653,26 +663,38 @@ def _synthesize_op_using_plugins( # or directly as a class inherited from HighLevelSynthesisPlugin (which then # does not need to be specified in entry_points). if isinstance(plugin_specifier, str): - if plugin_specifier not in hls_plugin_manager.method_names(op.name): + if plugin_specifier not in hls_plugin_manager.method_names(operation.name): raise TranspilerError( f"Specified method: {plugin_specifier} not found in available " - f"plugins for {op.name}" + f"plugins for {operation.name}" ) - plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) + plugin_method = hls_plugin_manager.method(operation.name, plugin_specifier) else: plugin_method = plugin_specifier - # Set the number of available clean and dirty auxiliary qubits via plugin args. + # The additional arguments we pass to every plugin include the list of global + # qubits over which the operation is defined, high-level-synthesis data and options, + # and the tracker that tracks the state for global qubits. + # + # Note: the difference between the argument "qubits" passed explicitely to "run" + # and "input_qubits" passed via "plugin_args" is that for backwards compatibility + # the former should be None if the synthesis is done before layout/routing. + # However, plugins may need access to the global qubits over which the operation + # is defined, as well as their state, in particular the plugin for AnnotatedOperations + # requires these arguments to be able to process the base operation recursively. + # + # We may want to refactor the inputs and the outputs for the plugins' "run" method, + # however this needs to be backwards-compatible. + plugin_args["input_qubits"] = input_qubits + plugin_args["_data"] = data + plugin_args["_qubit_tracker"] = tracker plugin_args["num_clean_ancillas"] = num_clean_ancillas plugin_args["num_dirty_ancillas"] = num_dirty_ancillas - plugin_args["_qubit_tracker"] = tracker - plugin_args["_data"] = data - plugin_args["input_qubits"] = input_qubits qubits = input_qubits if data.use_qubit_indices else None decomposition = plugin_method.run( - op, + operation, coupling_map=data.coupling_map, target=data.target, qubits=qubits, @@ -695,6 +717,11 @@ def _synthesize_op_using_plugins( best_decomposition = decomposition best_score = current_score + # A synthesis method may have potentially used available ancilla qubits. + # The following greedily grabs global qubits available. In the additional + # refactoring mentioned previously, we want each plugin to actually return + # the global qubits used, especially when the synthesis is done on the physical + # circuit, and the choice of which ancilla qubits to use really matters. output_qubits = input_qubits if best_decomposition is not None: if best_decomposition.num_qubits > len(input_qubits): From 1022ec418856b9bfc95593b3590afb902fda964f Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 20:29:20 +0200 Subject: [PATCH 22/32] pass over HLS::run --- .../passes/synthesis/high_level_synthesis.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index f0d843c4bd01..786df2248cda 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -305,8 +305,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ - # 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. + # Fast-path: check if HighLevelSynthesis can be skipped altogether. This is only + # done at the top-level since this does not track the qubit states. for node in dag.op_nodes(): qubits = tuple(dag.find_bit(q).index for q in node.qargs) if not _definitely_skip_node(self.data, node, qubits, dag): @@ -315,17 +315,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # The for-loop terminates without reaching the break statement return dag - # ToDo: try to avoid this conversion + # Regular-path: we synthesize the circuit recursively. Except for + # this conversion from DAGCircuit to QuantumCircuit and back, all + # the recursive functions work with QuantumCircuit objects only. circuit = dag_to_circuit(dag) input_qubits = list(range(circuit.num_qubits)) tracker = QubitTracker(num_qubits=dag.num_qubits()) if self.data.qubits_initially_zero: tracker.set_clean(input_qubits) - - (output_circuit, _) = _run(circuit, input_qubits, self.data, tracker) - assert isinstance(output_circuit, QuantumCircuit) - out_dag = circuit_to_dag(output_circuit) - return out_dag + output_circuit, _ = _run(circuit, input_qubits, self.data, tracker) + return circuit_to_dag(output_circuit) def _run( From 56272b6bc4b1718674cc1d80e2ac77c9d38b848a Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 24 Dec 2024 21:04:02 +0200 Subject: [PATCH 23/32] cleanup --- .../passes/synthesis/high_level_synthesis.py | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 786df2248cda..02df0e5fa4bf 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -315,7 +315,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # The for-loop terminates without reaching the break statement return dag - # Regular-path: we synthesize the circuit recursively. Except for + # Regular-path: we synthesize the circuit recursively. Except for # this conversion from DAGCircuit to QuantumCircuit and back, all # the recursive functions work with QuantumCircuit objects only. circuit = dag_to_circuit(dag) @@ -396,7 +396,7 @@ def _run( continue # Check if synthesis for this operation can be skipped - if _definitely_skip_op(data, op, op_qubits, input_circuit): + if _definitely_skip_op(op, op_qubits, data, input_circuit): output_circuit.append(op, inst.qubits, inst.clbits) tracker.set_dirty(op_qubits) continue @@ -571,8 +571,8 @@ def _synthesize_operation( def _get_custom_definition( operation: Operation, input_qubits: tuple[int], data: HLSData ) -> tuple[QuantumCircuit | None, tuple[int]]: - """Returns the definition for the given operation. - + """Returns the definition for the given operation. + Returns None if the operation is already supported or does not have the definition. """ @@ -617,7 +617,11 @@ def _methods_to_try(data: HLSData, name: str): def _synthesize_op_using_plugins( - operation: Operation, input_qubits: tuple[int], data: HLSData, tracker: QubitTracker, hls_methods: list + operation: Operation, + input_qubits: tuple[int], + data: HLSData, + tracker: QubitTracker, + hls_methods: list, ) -> tuple[QuantumCircuit | None, tuple[int]]: """ Attempts to synthesize an operation using plugin mechanism. @@ -672,12 +676,12 @@ def _synthesize_op_using_plugins( plugin_method = plugin_specifier # The additional arguments we pass to every plugin include the list of global - # qubits over which the operation is defined, high-level-synthesis data and options, - # and the tracker that tracks the state for global qubits. + # qubits over which the operation is defined, high-level-synthesis data and options, + # and the tracker that tracks the state for global qubits. # - # Note: the difference between the argument "qubits" passed explicitely to "run" + # Note: the difference between the argument "qubits" passed explicitely to "run" # and "input_qubits" passed via "plugin_args" is that for backwards compatibility - # the former should be None if the synthesis is done before layout/routing. + # the former should be None if the synthesis is done before layout/routing. # However, plugins may need access to the global qubits over which the operation # is defined, as well as their state, in particular the plugin for AnnotatedOperations # requires these arguments to be able to process the base operation recursively. @@ -720,7 +724,7 @@ def _synthesize_op_using_plugins( # The following greedily grabs global qubits available. In the additional # refactoring mentioned previously, we want each plugin to actually return # the global qubits used, especially when the synthesis is done on the physical - # circuit, and the choice of which ancilla qubits to use really matters. + # circuit, and the choice of which ancilla qubits to use really matters. output_qubits = input_qubits if best_decomposition is not None: if best_decomposition.num_qubits > len(input_qubits): @@ -735,12 +739,13 @@ def _synthesize_op_using_plugins( def _definitely_skip_node( data: HLSData, node: DAGOpNode, qubits: tuple[int] | None, dag: DAGCircuit ) -> bool: - """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will - attempt to synthesise it) without accessing its Python-space `Operation`. + """Fast-path determination of whether a DAG node can certainly be skipped + (i.e. nothing will attempt to synthesise it) without accessing its Python-space + `Operation`. - This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to - avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the - node (which is _most_ nodes).""" + This exists as a temporary measure to avoid Python-space `Operation` creation from a + `DAGOpNode` if we wouldn't do anything to the node (which is _most_ nodes). + """ if ( dag._has_calibration_for(node) @@ -774,16 +779,11 @@ def _definitely_skip_node( ) -# ToDo: try to avoid duplication with other function -def _definitely_skip_op(data: HLSData, op: Operation, qubits: tuple[int], dag: DAGCircuit) -> bool: - """Fast-path determination of whether a node can certainly be skipped (i.e. nothing will - attempt to synthesise it) without accessing its Python-space `Operation`. - - This is tightly coupled to `_recursively_handle_op`; it exists as a temporary measure to - avoid Python-space `Operation` creation from a `DAGOpNode` if we wouldn't do anything to the - node (which is _most_ nodes).""" +def _definitely_skip_op(op: Operation, qubits: tuple[int], data: HLSData, dag: DAGCircuit) -> bool: + """Check if an operation does not need to be synthesized.""" - assert qubits is not None + # It would be nice to avoid code duplication with the previous function, the difference is + # that this function is called on "Operation"s rather than "DAGOpNode"s. if ( len(qubits) < data.min_qubits @@ -817,13 +817,8 @@ def _definitely_skip_op(data: HLSData, op: Operation, qubits: tuple[int], dag: D def _instruction_supported(data: HLSData, name: str, qubits: tuple[int] | None) -> bool: + """Check whether operation is natively supported.""" # include path for when target exists but target.num_qubits is None (BasicSimulator) if data.target is None or data.target.num_qubits is None: return name in data.device_insts return data.target.instruction_supported(operation_name=name, qargs=qubits) - - -def _wrap_in_circuit(op: Operation) -> QuantumCircuit: - circuit = QuantumCircuit(op.num_qubits, op.num_clbits) - circuit.append(op, circuit.qubits, circuit.clbits) - return circuit From 2d3e800bbc022b9972a788ddc78a4066be9bc921 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 09:47:35 +0200 Subject: [PATCH 24/32] pass over annotated plugin --- .../passes/synthesis/high_level_synthesis.py | 13 +++- .../passes/synthesis/hls_plugins.py | 59 +++++++++++++------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 02df0e5fa4bf..5a8db5607d10 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -689,8 +689,8 @@ def _synthesize_op_using_plugins( # We may want to refactor the inputs and the outputs for the plugins' "run" method, # however this needs to be backwards-compatible. plugin_args["input_qubits"] = input_qubits - plugin_args["_data"] = data - plugin_args["_qubit_tracker"] = tracker + plugin_args["hls_data"] = data + plugin_args["qubit_tracker"] = tracker plugin_args["num_clean_ancillas"] = num_clean_ancillas plugin_args["num_dirty_ancillas"] = num_dirty_ancillas @@ -729,10 +729,17 @@ def _synthesize_op_using_plugins( if best_decomposition is not None: if best_decomposition.num_qubits > len(input_qubits): global_aux_qubits = tracker.borrow( - best_decomposition.num_qubits - len(output_qubits), output_qubits + best_decomposition.num_qubits - len(input_qubits), input_qubits ) output_qubits = output_qubits + global_aux_qubits + # This checks (in particular) that there is indeed a sufficient number + # of ancilla qubits to borrow from the tracker. + if best_decomposition.num_qubits != len(output_qubits): + raise TranspilerError( + "HighLevelSynthesis error: the result from 'synthesize_op_using_plugin' is incorrect." + ) + return (best_decomposition, output_qubits) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index c7decf0ba2ef..a883d01abb03 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1640,7 +1640,11 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** class AnnotatedSynthesisDefault(HighLevelSynthesisPlugin): - """Synthesize :class:`.AnnotatedOperation`""" + """Synthesize an :class:`.AnnotatedOperation` using the default synthesis algorithm. + + This plugin name is:``annotated.default`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): # pylint: disable=cyclic-import @@ -1651,34 +1655,51 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** operation = high_level_object modifiers = high_level_object.modifiers - tracker = options.get("_qubit_tracker", None) - data = options.get("_data") + + # The plugin needs additional information that is not yet passed via the run's method + # arguments: namely high-level-synthesis data and options, the global qubits over which + # the operation is defined, and the initial state of each global qubit. + tracker = options.get("qubit_tracker", None) + data = options.get("hls_data") input_qubits = options.get("input_qubits") - output_qubits = input_qubits + # output_qubits = input_qubits if len(modifiers) > 0: - # 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 - input_baseop_qubits = input_qubits[num_ctrl:] - - # Do not allow access to control qubits - tracker.disable(input_qubits[0:num_ctrl]) - synthesized_base_op, output_qubits = _synthesize_operation( - operation.base_op, input_baseop_qubits, data, tracker + total_power = sum(mod.power for mod in modifiers if isinstance(mod, PowerModifier)) + is_inverted = sum(1 for mod in modifiers if isinstance(mod, InverseModifier)) % 2 + + # The base operation cannot use control qubits as auxiliary qubits. + # In addition, when we have power or inverse modifiers, we need to set all of + # the operation's qubits to dirty. Note that synthesizing the base operation we + # can use additional auxiliary qubits, however they would always be returned to + # their previous state, so clean qubits remain clean after each for- or while- loop. + annotated_tracker = tracker.copy() + annotated_tracker.disable(input_qubits[:num_ctrl]) # do not access control qubits + if total_power != 0 or is_inverted: + annotated_tracker.set_dirty(input_qubits) + + # First, synthesize the base operation of this annotated operation. + # Note that synthesize_operation also returns the output qubits on which the + # operation is defined, however currently the plugin mechanism has no way + # to return these (and instead the upstream code greedily grabs some ancilla + # qubits from the circuit). We should refactor the plugin "run" iterface to + # return the actual ancilla qubits used. + synthesized_base_op, _ = _synthesize_operation( + operation.base_op, input_qubits[num_ctrl:], data, annotated_tracker ) + # The base operation does not need to be synthesized. + # For simplicity, we wrap the instruction into a circuit. Note that + # this should not deteriorate the quality of the result. if synthesized_base_op is None: - synthesized_base_op = operation.base_op + synthesized_base_op = _instruction_to_circuit(operation.base_op) assert not isinstance(synthesized_base_op, DAGCircuit) - # Restore access to control qubits. - tracker.enable(input_qubits[0:num_ctrl]) + tracker.set_dirty(input_qubits[num_ctrl:]) # This step currently does not introduce ancilla qubits. synthesized = _apply_annotations(data, synthesized_base_op, operation.modifiers) @@ -1698,6 +1719,8 @@ def _apply_annotations( is not an annotated operation). """ + assert isinstance(synthesized, QuantumCircuit) + for modifier in modifiers: if isinstance(modifier, InverseModifier): # Both QuantumCircuit and Gate have inverse method @@ -1727,7 +1750,7 @@ def _apply_annotations( ] controlled_circ.append(controlled_op, controlled_qubits) else: - + assert False assert (synthesized, Operation) synthesized = synthesized.control( num_ctrl_qubits=modifier.num_ctrl_qubits, From 5ea2ccbcbd2bd927b99a2b977637799671984478 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 10:33:04 +0200 Subject: [PATCH 25/32] cleanup --- .../passes/synthesis/high_level_synthesis.py | 16 ++- .../passes/synthesis/hls_plugins.py | 101 +++++++----------- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 5a8db5607d10..8aec8b36027a 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -356,7 +356,7 @@ def _run( if not isinstance(input_circuit, QuantumCircuit) or ( input_circuit.num_qubits != len(input_qubits) ): - raise TranspilerError("HighLevelSynthesis error: the input to 'run' is incorrect.") + raise TranspilerError("HighLevelSynthesis: the input to 'run' is incorrect.") # We iteratively process circuit instructions in the order they appear in the input circuit, # and add the synthesized instructions to the output circuit. Note that in the process the @@ -443,7 +443,7 @@ def _run( synthesized_circuit.num_qubits != len(synthesized_circuit_qubits) ): raise TranspilerError( - "HighLevelSynthesis error: the output from 'synthesize_operation' is incorrect." + "HighLevelSynthesis: the output from 'synthesize_operation' is incorrect." ) # If the synthesized circuit uses (auxiliary) global qubits that are not in the output circuit, @@ -473,7 +473,7 @@ def _run( # Another pedantic check that can possibly be removed. if output_circuit.num_qubits != len(output_qubits): - raise TranspilerError("HighLevelSynthesis error: the input from 'run' is incorrect.") + raise TranspilerError("HighLevelSynthesis: the input from 'run' is incorrect.") return (output_circuit, output_qubits) @@ -505,7 +505,7 @@ def _synthesize_operation( if operation.num_qubits != len(input_qubits): raise TranspilerError( - "HighLevelSynthesis error: the input to 'synthesize_operation' is incorrect." + "HighLevelSynthesis: the input to 'synthesize_operation' is incorrect." ) # Synthesize the operation: @@ -542,9 +542,7 @@ def _synthesize_operation( if not isinstance(output_circuit, QuantumCircuit) or ( output_circuit.num_qubits != len(output_qubits) ): - raise TranspilerError( - "HighLevelSynthesis error: the intermediate circuit is incorrect." - ) + raise TranspilerError("HighLevelSynthesis: the intermediate circuit is incorrect.") if output_circuit is None: # if we didn't synthesize, there is nothing to do. @@ -562,7 +560,7 @@ def _synthesize_operation( if (output_circuit is not None) and (output_circuit.num_qubits != len(output_qubits)): raise TranspilerError( - "HighLevelSynthesis error: the output of 'synthesize_operation' is incorrect." + "HighLevelSynthesis: the output of 'synthesize_operation' is incorrect." ) return output_circuit, output_qubits @@ -737,7 +735,7 @@ def _synthesize_op_using_plugins( # of ancilla qubits to borrow from the tracker. if best_decomposition.num_qubits != len(output_qubits): raise TranspilerError( - "HighLevelSynthesis error: the result from 'synthesize_op_using_plugin' is incorrect." + "HighLevelSynthesis: the result from 'synthesize_op_using_plugin' is incorrect." ) return (best_decomposition, output_qubits) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index a883d01abb03..9bf48dfa1a61 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -1662,7 +1662,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** tracker = options.get("qubit_tracker", None) data = options.get("hls_data") input_qubits = options.get("input_qubits") - # output_qubits = input_qubits if len(modifiers) > 0: num_ctrl = sum( @@ -1697,97 +1696,77 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** if synthesized_base_op is None: synthesized_base_op = _instruction_to_circuit(operation.base_op) - assert not isinstance(synthesized_base_op, DAGCircuit) - tracker.set_dirty(input_qubits[num_ctrl:]) - # This step currently does not introduce ancilla qubits. - synthesized = _apply_annotations(data, synthesized_base_op, operation.modifiers) - assert isinstance(synthesized, QuantumCircuit) + # This step currently does not introduce ancilla qubits. However it makes + # a lot of sense to allow this in the future. + synthesized = _apply_annotations(synthesized_base_op, operation.modifiers) + + if not isinstance(synthesized, QuantumCircuit): + raise TranspilerError( + "HighLevelSynthesis: problem with the default plugin for annotated operations." + ) return synthesized return None -def _apply_annotations( - data: "HLSData", synthesized: Operation | QuantumCircuit, modifiers: list[Modifier] -) -> QuantumCircuit: +def _apply_annotations(circuit: QuantumCircuit, modifiers: list[Modifier]) -> QuantumCircuit: """ - Recursively synthesizes annotated operations. - Returns either the synthesized operation or None (which occurs when the operation - is not an annotated operation). + Applies modifiers to a quantum circuit. """ - assert isinstance(synthesized, QuantumCircuit) + if not isinstance(circuit, QuantumCircuit): + raise TranspilerError("HighLevelSynthesis: incorrect input to 'apply_annotations'.") for modifier in modifiers: if isinstance(modifier, InverseModifier): - # Both QuantumCircuit and Gate have inverse method - synthesized = synthesized.inverse() + circuit = circuit.inverse() elif isinstance(modifier, ControlModifier): - # Both QuantumCircuit and Gate have control method, however for circuits - # it is more efficient to avoid constructing the controlled quantum circuit. - - assert synthesized.num_clbits == 0 - - controlled_circ = QuantumCircuit(modifier.num_ctrl_qubits + synthesized.num_qubits) - - if isinstance(synthesized, QuantumCircuit): - for inst in synthesized: - inst_op = inst.operation - inst_qubits = inst.qubits - controlled_op = inst_op.control( - num_ctrl_qubits=modifier.num_ctrl_qubits, - label=None, - ctrl_state=modifier.ctrl_state, - annotated=False, - ) - controlled_qubits = list(range(0, modifier.num_ctrl_qubits)) + [ - modifier.num_ctrl_qubits + synthesized.find_bit(q).index - for q in inst_qubits - ] - controlled_circ.append(controlled_op, controlled_qubits) - else: - assert False - assert (synthesized, Operation) - synthesized = synthesized.control( + if circuit.num_clbits > 0: + raise TranspilerError( + "HighLevelSynthesis: cannot control a circuit with classical bits." + ) + + # Apply the control modifier to each gate in the circuit. + controlled_circuit = QuantumCircuit(modifier.num_ctrl_qubits + circuit.num_qubits) + for inst in circuit: + inst_op = inst.operation + inst_qubits = inst.qubits + controlled_op = inst_op.control( num_ctrl_qubits=modifier.num_ctrl_qubits, label=None, ctrl_state=modifier.ctrl_state, annotated=False, ) + controlled_qubits = list(range(0, modifier.num_ctrl_qubits)) + [ + modifier.num_ctrl_qubits + circuit.find_bit(q).index for q in inst_qubits + ] + controlled_circuit.append(controlled_op, controlled_qubits) - controlled_circ.append(synthesized, controlled_circ.qubits) - - synthesized = controlled_circ + circuit = controlled_circuit - if isinstance(synthesized, AnnotatedOperation): + if isinstance(circuit, AnnotatedOperation): raise TranspilerError( - "HighLevelSynthesis failed to synthesize the control modifier." + "HighLevelSynthesis: failed to synthesize the control modifier." ) elif isinstance(modifier, PowerModifier): - # QuantumCircuit has power method, and Gate needs to be converted - # to a quantum circuit. - if not isinstance(synthesized, QuantumCircuit): - synthesized = _instruction_to_circuit(synthesized) - - synthesized = synthesized.power(modifier.power) + circuit = circuit.power(modifier.power) else: - raise TranspilerError(f"Unknown modifier {modifier}.") + raise TranspilerError(f"HighLevelSynthesis: Unknown modifier {modifier}.") - if not isinstance(synthesized, QuantumCircuit): - circuit = QuantumCircuit(synthesized.num_qubits) - circuit.append(synthesized, circuit.qubits) - return circuit + if not isinstance(circuit, QuantumCircuit): + raise TranspilerError("HighLevelSynthesis: incorrect output of 'apply_annotations'.") - return synthesized + return circuit -def _instruction_to_circuit(inst: Instruction) -> QuantumCircuit: - circuit = QuantumCircuit(inst.num_qubits, inst.num_clbits) - circuit.append(inst, circuit.qubits, circuit.clbits) +def _instruction_to_circuit(op: Operation) -> QuantumCircuit: + """Wraps a single operation into a quantum circuit.""" + circuit = QuantumCircuit(op.num_qubits, op.num_clbits) + circuit.append(op, circuit.qubits, circuit.clbits) return circuit From 1ffeba795c3c0dd3e603cd633f244cffd028bad5 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 10:56:37 +0200 Subject: [PATCH 26/32] improving comment --- .../transpiler/passes/synthesis/high_level_synthesis.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 8aec8b36027a..03623262348e 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -550,8 +550,12 @@ def _synthesize_operation( pass else: # Output circuit is a quantum circuit which we want to process recursively. - # We save the current state of the tracker to be able to return the ancilla - # qubits to the current positions. + # Currently, neither 'synthesize_op_using_plugins' nor 'get_custom_definition' + # update the tracker (we might want to change this in the future), which makes + # sense because we have not synthesized the output circuit yet. + # So we pass the tracker to '_run' but make sure to restore the status of + # clean ancilla qubits after the circuit is synthesized. In order to do that, + # we save the current state of the tracker. saved_tracker = tracker.copy() output_circuit, output_qubits = _run(output_circuit, output_qubits, data, tracker) From 39e552bf75507594d3354bb097d606a0ff1e4a9b Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 11:30:04 +0200 Subject: [PATCH 27/32] fixing pylint --- .../passes/synthesis/high_level_synthesis.py | 21 ++++--------------- .../passes/synthesis/hls_plugins.py | 5 ----- .../circuit/library/test_multipliers.py | 1 - 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 03623262348e..6f5bc43039b8 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -17,32 +17,21 @@ from __future__ import annotations import typing -from functools import partial from collections.abc import Callable import numpy as np -from qiskit.circuit.annotated_operation import Modifier from qiskit.circuit.controlflow.control_flow import ControlFlowOp from qiskit.circuit.operation import Operation -from qiskit.circuit.instruction import Instruction 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, Qubit -from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError -from qiskit.circuit.annotated_operation import ( - AnnotatedOperation, - InverseModifier, - ControlModifier, - PowerModifier, -) - from qiskit._accelerate.high_level_synthesis import QubitTracker from .plugin import HighLevelSynthesisPluginManager @@ -266,8 +255,6 @@ def __init__( if target is not None: coupling_map = target.build_coupling_map() - else: - coupling_map = coupling_map top_level_only = basis_gates is None and target is None @@ -396,7 +383,7 @@ def _run( continue # Check if synthesis for this operation can be skipped - if _definitely_skip_op(op, op_qubits, data, input_circuit): + if _definitely_skip_op(op, op_qubits, data): output_circuit.append(op, inst.qubits, inst.clbits) tracker.set_dirty(op_qubits) continue @@ -431,8 +418,8 @@ def _run( op, op_qubits, data, tracker ) - # If the synthesis did not change anything, we add the operation to the output circuit and update the - # qubit tracker. + # If the synthesis did not change anything, we add the operation to the output circuit + # and update the qubit tracker. if synthesized_circuit is None: output_circuit.append(op, inst.qubits, inst.clbits) tracker.set_dirty(op_qubits) @@ -788,7 +775,7 @@ def _definitely_skip_node( ) -def _definitely_skip_op(op: Operation, qubits: tuple[int], data: HLSData, dag: DAGCircuit) -> bool: +def _definitely_skip_op(op: Operation, qubits: tuple[int], data: HLSData) -> bool: """Check if an operation does not need to be synthesized.""" # It would be nice to avoid code duplication with the previous function, the difference is diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 9bf48dfa1a61..5bf4e3f3c187 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -412,12 +412,8 @@ import numpy as np import rustworkx as rx -from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.operation import Operation -from qiskit.circuit.instruction import Instruction from qiskit.circuit.library import ( LinearFunction, QFTGate, @@ -1625,7 +1621,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** preserve_order = options.get("preserve_order", True) upto_clifford = options.get("upto_clifford", False) upto_phase = options.get("upto_phase", False) - input_qubits = options.get("input_qubits") resynth_clifford_method = options.get("resynth_clifford_method", 1) return synth_pauli_network_rustiq( diff --git a/test/python/circuit/library/test_multipliers.py b/test/python/circuit/library/test_multipliers.py index f6301470c5a5..defa8c12f105 100644 --- a/test/python/circuit/library/test_multipliers.py +++ b/test/python/circuit/library/test_multipliers.py @@ -13,7 +13,6 @@ """Test multiplier circuits.""" import unittest -import re import numpy as np from ddt import ddt, data, unpack From 919b54939283d2f44863fb847a9a24c563cf94b4 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 12:42:46 +0200 Subject: [PATCH 28/32] remove print statements in tests --- test/python/transpiler/test_high_level_synthesis.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index cc3371ee479d..2327e5096188 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -1126,9 +1126,6 @@ def test_nested_controls(self): circuit = QuantumCircuit(5) circuit.append(lazy_gate2, [0, 1, 2, 3, 4]) transpiled_circuit = HighLevelSynthesis()(circuit) - print(transpiled_circuit) - print(type(transpiled_circuit[0])) - print(transpiled_circuit[0]) expected_circuit = QuantumCircuit(5) expected_circuit.append(SwapGate().control(2).control(1), [0, 1, 2, 3, 4]) self.assertEqual(transpiled_circuit, expected_circuit) From 9f2ef8d9c7635e8617cbd518625868b56225ad6d Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 14:08:14 +0200 Subject: [PATCH 29/32] fmt --- crates/accelerate/src/high_level_synthesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/high_level_synthesis.rs b/crates/accelerate/src/high_level_synthesis.rs index 2a43d696beda..5734c5a9fd5d 100644 --- a/crates/accelerate/src/high_level_synthesis.rs +++ b/crates/accelerate/src/high_level_synthesis.rs @@ -251,7 +251,7 @@ impl QubitContext { /// Pretty-prints pub fn __str__(&self) -> String { let mut out = String::from("QubitContext("); - for (q_loc, q_glob ) in self.local_to_global.iter().enumerate() { + for (q_loc, q_glob) in self.local_to_global.iter().enumerate() { out.push_str(&q_loc.to_string()); out.push(':'); out.push(' '); From c5f0f0e21a46184f8157cb76d48705823e410de1 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 15:01:00 +0200 Subject: [PATCH 30/32] fix + test for controlling circuits with nontrivial phase --- .../transpiler/passes/synthesis/hls_plugins.py | 10 ++++++++++ .../transpiler/test_high_level_synthesis.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index 5bf4e3f3c187..b242e7b036a6 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -425,6 +425,7 @@ HalfAdderGate, FullAdderGate, MultiplierGate, + GlobalPhaseGate, ) from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -1727,6 +1728,15 @@ def _apply_annotations(circuit: QuantumCircuit, modifiers: list[Modifier]) -> Qu # Apply the control modifier to each gate in the circuit. controlled_circuit = QuantumCircuit(modifier.num_ctrl_qubits + circuit.num_qubits) + if circuit.global_phase != 0: + controlled_op = GlobalPhaseGate(circuit.global_phase).control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + annotated=False, + ) + controlled_qubits = list(range(0, modifier.num_ctrl_qubits)) + controlled_circuit.append(controlled_op, controlled_qubits) for inst in circuit: inst_op = inst.operation inst_qubits = inst.qubits diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 2327e5096188..7bd59a66d9dc 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -1617,6 +1617,24 @@ def test_annotated_circuit(self): qct = pass_(qc) self.assertEqual(Statevector(qc), Statevector(qct)) + def test_annotated_circuit_with_phase(self): + inner = QuantumCircuit(2) + inner.global_phase = 1 + inner.h(0) + inner.cx(0, 1) + gate = inner.to_gate() + + qc1 = QuantumCircuit(3) + qc1.append(gate.control(annotated=False), [0, 1, 2]) + qct1 = HighLevelSynthesis(basis_gates=["cx", "u"])(qc1) + + qc2 = QuantumCircuit(3) + qc2.append(gate.control(annotated=True), [0, 1, 2]) + qct2 = HighLevelSynthesis(basis_gates=["cx", "u"])(qc2) + + self.assertEqual(Operator(qc1), Operator(qc2)) + self.assertEqual(Operator(qct1), Operator(qct2)) + def test_annotated_rec(self): """Test synthesis with annotated custom gates and recursion.""" inner2 = QuantumCircuit(2) From b59dbe6ae62b917d7261d112ecde92a8cd2d1dc5 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 15:16:09 +0200 Subject: [PATCH 31/32] adding release notes --- releasenotes/notes/improve-hls-pass-620389dde24e9707.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/improve-hls-pass-620389dde24e9707.yaml diff --git a/releasenotes/notes/improve-hls-pass-620389dde24e9707.yaml b/releasenotes/notes/improve-hls-pass-620389dde24e9707.yaml new file mode 100644 index 000000000000..b65e81709a2d --- /dev/null +++ b/releasenotes/notes/improve-hls-pass-620389dde24e9707.yaml @@ -0,0 +1,6 @@ +--- +features_transpiler: + - | + The :class:`.HighLevelSynthesis` transpiler pass now synthesizes + objects of type :class:`~.AnnotatedOperation` objects via the + plugin interface. \ No newline at end of file From 27c66baf1eecd91b20bad2da1748fbe60b3e68b4 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 25 Dec 2024 16:17:35 +0200 Subject: [PATCH 32/32] adding test function docstring --- test/python/transpiler/test_high_level_synthesis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 7bd59a66d9dc..0e064c7738a7 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -1618,6 +1618,7 @@ def test_annotated_circuit(self): self.assertEqual(Statevector(qc), Statevector(qct)) def test_annotated_circuit_with_phase(self): + """Test controlled-annotated circuits with global phase.""" inner = QuantumCircuit(2) inner.global_phase = 1 inner.h(0)