diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 98c9b4d4e452..04bb934a0211 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -137,7 +137,7 @@ def control( gate.definition.data[0].operation.params[0], q_control, q_target[bit_indices[qargs[0]]], - use_basis_gates=True, + use_basis_gates=False, ) elif gate.name == "ry": controlled_circ.mcry( @@ -146,14 +146,14 @@ def control( q_target[bit_indices[qargs[0]]], q_ancillae, mode="noancilla", - use_basis_gates=True, + use_basis_gates=False, ) elif gate.name == "rz": controlled_circ.mcrz( gate.definition.data[0].operation.params[0], q_control, q_target[bit_indices[qargs[0]]], - use_basis_gates=True, + use_basis_gates=False, ) continue elif gate.name == "p": diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py index 6e31c99005b3..8746e51c48db 100644 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py @@ -200,7 +200,7 @@ def _mcsu2_real_diagonal( circuit.h(target) if use_basis_gates: - circuit = transpile(circuit, basis_gates=["p", "u", "cx"]) + circuit = transpile(circuit, basis_gates=["p", "u", "cx"], qubits_initially_zero=False) return circuit diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index cb2c19bf51e9..39eda0ab923a 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -377,7 +377,8 @@ def _define(self): q_target = self.num_ctrl_qubits new_target = q_target for k in range(self.num_ctrl_qubits): - qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=True) + # Note: it's better *not* to run transpile recursively + qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=False) new_target = q_controls.pop() qc.p(lam / (2**self.num_ctrl_qubits), new_target) else: # in this case type(lam) is ParameterValueType diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index fd93fde5989f..929089c4ac41 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -66,6 +66,7 @@ def transpile( # pylint: disable=too-many-return-statements optimization_method: Optional[str] = None, ignore_backend_supplied_default_methods: bool = False, num_processes: Optional[int] = None, + qubits_initially_zero: bool = True, ) -> _CircuitT: """Transpile one or more circuits, according to some desired transpilation targets. @@ -284,6 +285,7 @@ def callback_func(**kwargs): ``num_processes`` in the user configuration file, and the ``QISKIT_NUM_PROCS`` environment variable. If set to ``None`` the system default or local user configuration will be used. + qubits_initially_zero: Indicates whether the input circuit is zero-initialized. Returns: The transpiled circuit(s). @@ -386,6 +388,7 @@ def callback_func(**kwargs): init_method=init_method, optimization_method=optimization_method, dt=dt, + qubits_initially_zero=qubits_initially_zero, ) out_circuits = pm.run(circuits, callback=callback, num_processes=num_processes) diff --git a/qiskit/synthesis/unitary/qsd.py b/qiskit/synthesis/unitary/qsd.py index b6b31aaa4fec..41a1fb77356c 100644 --- a/qiskit/synthesis/unitary/qsd.py +++ b/qiskit/synthesis/unitary/qsd.py @@ -255,7 +255,9 @@ def _apply_a2(circ): from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate decomposer = two_qubit_decompose_up_to_diagonal - ccirc = transpile(circ, basis_gates=["u", "cx", "qsd2q"], optimization_level=0) + ccirc = transpile( + circ, basis_gates=["u", "cx", "qsd2q"], optimization_level=0, qubits_initially_zero=False + ) ind2q = [] # collect 2q instrs for i, instruction in enumerate(ccirc.data): diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 6974b1cce06c..247898182cba 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -162,12 +162,15 @@ from __future__ import annotations import typing -from typing import Optional, Union, List, Tuple, Callable, Sequence +from functools import partial +from collections.abc import Callable import numpy as np import rustworkx as rx +from qiskit.circuit.annotated_operation import Modifier 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 @@ -212,6 +215,7 @@ ) from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin +from .qubit_tracker import QubitTracker if typing.TYPE_CHECKING: from qiskit.dagcircuit import DAGOpNode @@ -268,7 +272,7 @@ def __init__( self, use_default_on_unspecified: bool = True, plugin_selection: str = "sequential", - plugin_evaluation_fn: Optional[Callable[[QuantumCircuit], int]] = None, + plugin_evaluation_fn: Callable[[QuantumCircuit], int] | None = None, **kwargs, ): """Creates a high-level-synthesis config. @@ -304,7 +308,7 @@ def set_methods(self, hls_name, hls_methods): class HighLevelSynthesis(TransformationPass): - """Synthesize higher-level objects and unroll custom definitions. + r"""Synthesize higher-level objects and unroll custom definitions. The input to this pass is a DAG that may contain higher-level objects, including abstract mathematical objects (e.g., objects of type :class:`.LinearFunction`), @@ -345,19 +349,35 @@ class HighLevelSynthesis(TransformationPass): abstract mathematical objects and annotated operations, without descending into the gate ``definitions``. This is consistent with the older behavior of the pass, allowing to synthesize some higher-level objects using plugins and leaving the other gates untouched. + + The high-level-synthesis passes information about available auxiliary qubits, and whether their + state is clean (defined as :math:`|0\rangle`) or dirty (unknown state) to the synthesis routine + via the respective arguments ``"num_clean_ancillas"`` and ``"num_dirty_ancillas"``. + If ``qubits_initially_zero`` is ``True`` (default), the qubits are assumed to be in the + :math:`|0\rangle` state. When appending a synthesized block using auxiliary qubits onto the + circuit, we first use the clean auxiliary qubits. + + .. note:: + + Synthesis methods are assumed to maintain the state of the auxiliary qubits. + Concretely this means that clean auxiliary qubits must still be in the :math:`|0\rangle` + state after the synthesized block, while dirty auxiliary qubits are re-used only + as dirty qubits. + """ def __init__( self, - hls_config: Optional[HLSConfig] = None, - coupling_map: Optional[CouplingMap] = None, - target: Optional[Target] = None, + hls_config: HLSConfig | None = None, + coupling_map: CouplingMap | None = None, + target: Target | None = None, use_qubit_indices: bool = False, - equivalence_library: Optional[EquivalenceLibrary] = None, - basis_gates: Optional[List[str]] = None, + equivalence_library: EquivalenceLibrary | None = None, + basis_gates: list[str] | None = None, min_qubits: int = 0, + qubits_initially_zero: bool = True, ): - """ + r""" HighLevelSynthesis initializer. Args: @@ -376,6 +396,9 @@ def __init__( Ignored if ``target`` is also specified. min_qubits: The minimum number of qubits for operations in the input dag to translate. + qubits_initially_zero: Indicates whether the qubits are initially in the state + :math:`|0\rangle`. This allows the high-level-synthesis to use clean auxiliary qubits + (i.e. in the zero state) to synthesize an operation. """ super().__init__() @@ -390,6 +413,7 @@ def __init__( self._coupling_map = coupling_map self._target = target self._use_qubit_indices = use_qubit_indices + self.qubits_initially_zero = qubits_initially_zero if target is not None: self._coupling_map = self._target.build_coupling_map() self._equiv_lib = equivalence_library @@ -418,151 +442,221 @@ 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). """ + qubits = tuple(dag.find_bit(q).index for q in dag.qubits) + if self.qubits_initially_zero: + clean, dirty = set(qubits), set() + else: + clean, dirty = set(), set(qubits) + + tracker = QubitTracker(qubits=qubits, clean=clean, dirty=dirty) + return self._run(dag, tracker) + + def _run(self, dag: DAGCircuit, tracker: QubitTracker) -> DAGCircuit: + # Start by analyzing the nodes in the DAG. This for-loop is a first version of a potentially + # more elaborate approach to find good operation/ancilla allocations. It greedily iterates + # over the nodes, checking whether we can synthesize them, while keeping track of the + # qubit states. It does not trade-off allocations and just gives all available qubits + # to the current operation (a "the-first-takes-all" approach). + synthesized_nodes = {} + + for node in dag.topological_op_nodes(): + qubits = tuple(dag.find_bit(q).index for q in node.qargs) + synthesized = None + used_qubits = None + + # check if synthesis for the operation can be skipped + if ( + dag.has_calibration_for(node) + or len(node.qargs) < self._min_qubits + or node.is_directive() + or self._definitely_skip_node(node, qubits) + ): + pass - # copy dag_op_nodes because we are modifying the DAG below - dag_op_nodes = dag.op_nodes() - - for node in dag_op_nodes: - if node.is_control_flow(): - node.op = control_flow.map_blocks(self.run, node.op) - continue + # next check control flow + elif node.is_control_flow(): + node.op = control_flow.map_blocks( + partial(self._run, tracker=tracker.copy()), node.op + ) - if node.is_directive(): - continue + # now we are free to synthesize + else: + # this returns the synthesized operation and the qubits it acts on -- note that this + # may be different than the original qubits, since we may use auxiliary qubits + synthesized, used_qubits = self._synthesize_operation(node.op, qubits, tracker) + + # if the synthesis changed the operation (i.e. it is not None), store the result + # and mark the operation qubits as used + if synthesized is not None: + synthesized_nodes[node] = (synthesized, used_qubits) + tracker.used(qubits) # assumes that auxiliary are returned in the same state + + # if the synthesis did not change anything, just update the qubit tracker + # other cases can be added: swaps, controlled gates (e.g. if control is 0), ... + else: + if node.op.name in ["id", "delay", "barrier"]: + pass # tracker not updated, these are no-ops + elif node.op.name == "reset": + tracker.reset(qubits) # reset qubits to 0 + else: + tracker.used(qubits) # any other op used the clean state up + + # we did not change anything just return the input + if len(synthesized_nodes) == 0: + return dag + + # Otherwise we will rebuild with the new operations. Note that we could also + # check if no operation changed in size and substitute in-place, but rebuilding is + # generally as fast or faster, unless very few operations are changed. + out = dag.copy_empty_like() + index_to_qubit = dict(enumerate(dag.qubits)) + + for node in dag.topological_op_nodes(): + if node in synthesized_nodes: + op, qubits = synthesized_nodes[node] + qargs = tuple(index_to_qubit[index] for index in qubits) + if isinstance(op, Operation): + out.apply_operation_back(op, qargs, cargs=[]) + continue + + if isinstance(op, QuantumCircuit): + op = circuit_to_dag(op, copy_operations=False) + + if isinstance(op, DAGCircuit): + qubit_map = { + qubit: index_to_qubit[index] for index, qubit in zip(qubits, 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 RuntimeError(f"Unexpected synthesized type: {type(op)}") + else: + out.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: - continue + return out - qubits = ( - [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None + def _synthesize_operation( + self, + operation: Operation, + qubits: tuple[int], + tracker: QubitTracker, + ) -> tuple[QuantumCircuit | Operation | DAGCircuit | None, list[int] | 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``. + synthesized = None + + # Try synthesizing via AnnotatedOperation. This is faster than an isinstance check + # but a bit less safe since someone could create operations with a ``modifiers`` attribute. + if len(modifiers := getattr(operation, "modifiers", [])) > 0: + # The base operation must be synthesized without using potential control qubits + # used in the modifiers. + num_ctrl = sum( + mod.num_ctrl_qubits for mod in modifiers if isinstance(mod, ControlModifier) ) + baseop_qubits = qubits[num_ctrl:] # reminder: control qubits are the first ones + baseop_tracker = tracker.copy(drop=qubits[:num_ctrl]) # no access to control qubits - if self._definitely_skip_node(node, qubits): - continue - - decomposition, modified = self._recursively_handle_op(node.op, qubits) + # get qubits of base operation + synthesized_base_op, _ = self._synthesize_operation( + operation.base_op, baseop_qubits, baseop_tracker + ) + 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 not modified: - continue + synthesized = self._apply_annotations(synthesized_base_op, operation.modifiers) - if isinstance(decomposition, QuantumCircuit): - dag.substitute_node_with_dag( - node, circuit_to_dag(decomposition, copy_operations=False) + # 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 + if len(hls_methods := self._methods_to_try(operation.name)) > 0: + synthesized = self._synthesize_op_using_plugins( + hls_methods, + operation, + indices, + tracker.num_clean(qubits), + tracker.num_dirty(qubits), ) - elif isinstance(decomposition, DAGCircuit): - dag.substitute_node_with_dag(node, decomposition) - elif isinstance(decomposition, Operation): - dag.substitute_node(node, decomposition) - return dag + # If HLS did not apply, or was unsuccessful, try unrolling custom definitions. + if synthesized is None and not self._top_level_only: + synthesized = self._unroll_custom_definition(operation, indices) - def _definitely_skip_node(self, node: DAGOpNode, qubits: Sequence[int] | None) -> 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`. + if synthesized is None: + # if we didn't synthesize, there was nothing to unroll, so just set the used qubits + used_qubits = qubits - 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).""" - return ( - # The fast path is just for Rust-space standard gates (which excludes - # `AnnotatedOperation`). - node.is_standard_gate() - # If it's a controlled gate, we might choose to do funny things to it. - and not node.is_controlled_gate() - # 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 ( - self._instruction_supported(node.name, qubits) - # 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. - or ( - self._equiv_lib is not None - and equivalence.Key(name=node.name, num_qubits=node.num_qubits) - in self._equiv_lib._key_to_node_index + else: + # if it has been synthesized, recurse and finally store the decomposition + if isinstance(synthesized, Operation): + re_synthesized, qubits = self._synthesize_operation( + synthesized, qubits, tracker.copy() ) - ) - ) - - def _instruction_supported(self, name: str, qubits: Sequence[int]) -> bool: - qubits = tuple(qubits) if qubits is not None else None - # 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) - - def _recursively_handle_op( - self, op: Operation, qubits: Optional[List] = None - ) -> Tuple[Union[QuantumCircuit, DAGCircuit, Operation], bool]: - """Recursively synthesizes a single operation. - - Note: the reason that this function accepts an operation and not a dag node - is that it's also used for synthesizing the base operation for an annotated - gate (i.e. no dag node is available). + if re_synthesized is not None: + synthesized = re_synthesized + used_qubits = qubits + + elif isinstance(synthesized, QuantumCircuit): + aux_qubits = tracker.borrow(synthesized.num_qubits - len(qubits), qubits) + used_qubits = qubits + tuple(aux_qubits) + as_dag = circuit_to_dag(synthesized, copy_operations=False) + + # map used qubits to subcircuit + new_qubits = [as_dag.find_bit(q).index for q in as_dag.qubits] + qubit_map = dict(zip(used_qubits, new_qubits)) + + synthesized = self._run(as_dag, tracker.copy(qubit_map)) + if synthesized.num_qubits() != len(used_qubits): + raise RuntimeError( + f"Mismatching number of qubits, using {synthesized.num_qubits()} " + f"but have {len(used_qubits)}." + ) - There are several possible results: + else: + raise RuntimeError(f"Unexpected synthesized type: {type(synthesized)}") - - The given operation is unchanged: e.g., it is supported by the target or is - in the equivalence library - - The result is a quantum circuit: e.g., synthesizing Clifford using plugin - - The result is a DAGCircuit: e.g., when unrolling custom gates - - The result is an Operation: e.g., adding control to CXGate results in CCXGate - - The given operation could not be synthesized, raising a transpiler error + if synthesized is not None and used_qubits is None: + raise RuntimeError("Failed to find qubit indices on", synthesized) - The function returns the result of the synthesis (either a quantum circuit or - an Operation), and, as an optimization, a boolean indicating whether - synthesis did anything. + return synthesized, used_qubits - The function is recursive, for example synthesizing an annotated operation - involves synthesizing its "base operation" which might also be - an annotated operation. - """ - - # WARNING: if adding new things in here, ensure that `_definitely_skip_node` is also - # up-to-date. - - # Try to apply plugin mechanism - decomposition = self._synthesize_op_using_plugins(op, qubits) - if decomposition is not None: - return decomposition, True - - # Handle annotated operations - decomposition = self._synthesize_annotated_op(op) - if decomposition: - return decomposition, True - - # Don't do anything else if processing only top-level - if self._top_level_only: - return op, False - - # For non-controlled-gates, check if it's already supported by the target - # or is in equivalence library - controlled_gate_open_ctrl = isinstance(op, ControlledGate) and op._open_ctrl - if not controlled_gate_open_ctrl: - if self._instruction_supported(op.name, qubits) or ( - self._equiv_lib is not None and self._equiv_lib.has_entry(op) - ): - return op, False + def _unroll_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._equiv_lib is not None and self._equiv_lib.has_entry(inst)): + return None # we support this operation already + # if not, try to get the definition try: - # extract definition - definition = op.definition - except TypeError as err: - raise TranspilerError( - f"HighLevelSynthesis was unable to extract definition for {op.name}: {err}" - ) from err - except AttributeError: - # definition is None - definition = None + 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 {op}.") + raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {inst}.") - dag = circuit_to_dag(definition, copy_operations=False) - dag = self.run(dag) - return dag, True + return definition def _methods_to_try(self, name: str): """Get a sequence of methods to try for a given op name.""" @@ -581,19 +675,30 @@ def _methods_to_try(self, name: str): return [] def _synthesize_op_using_plugins( - self, op: Operation, qubits: List - ) -> Union[QuantumCircuit, None]: + 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. - Returns either the synthesized circuit or None (which occurs when no - synthesis methods are available or specified). + + 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 = self.hls_plugin_manager best_decomposition = None best_score = np.inf - for method in self._methods_to_try(op.name): + 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., @@ -622,6 +727,10 @@ def _synthesize_op_using_plugins( 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._coupling_map, @@ -648,77 +757,85 @@ def _synthesize_op_using_plugins( return best_decomposition - def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: + 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). """ - if isinstance(op, AnnotatedOperation): - # Recursively handle the base operation - # This results in QuantumCircuit, DAGCircuit or Gate - synthesized_op, _ = self._recursively_handle_op(op.base_op, qubits=None) - - if isinstance(synthesized_op, AnnotatedOperation): - raise TranspilerError( - "HighLevelSynthesis failed to synthesize the base operation of" - " 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, ) - for modifier in op.modifiers: - # If we have a DAGCircuit at this point, convert it to QuantumCircuit - if isinstance(synthesized_op, DAGCircuit): - synthesized_op = dag_to_circuit(synthesized_op, copy_operations=False) - - if isinstance(modifier, InverseModifier): - # Both QuantumCircuit and Gate have inverse method - synthesized_op = synthesized_op.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_op, QuantumCircuit): - synthesized_op = synthesized_op.to_gate() - - synthesized_op = synthesized_op.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_op, 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) - # Unrolling - synthesized_op, _ = self._recursively_handle_op(synthesized_op) - - elif isinstance(modifier, PowerModifier): - # QuantumCircuit has power method, and Gate needs to be converted - # to a quantum circuit. - if isinstance(synthesized_op, QuantumCircuit): - qc = synthesized_op - else: - qc = QuantumCircuit(synthesized_op.num_qubits, synthesized_op.num_clbits) - qc.append( - synthesized_op, - range(synthesized_op.num_qubits), - range(synthesized_op.num_clbits), - ) + synthesized = synthesized.power(modifier.power) - qc = qc.power(modifier.power) - synthesized_op = qc.to_gate() + else: + raise TranspilerError(f"Unknown modifier {modifier}.") - # Unrolling - synthesized_op, _ = self._recursively_handle_op(synthesized_op) + return synthesized - else: - raise TranspilerError(f"Unknown modifier {modifier}.") + def _definitely_skip_node(self, node: DAGOpNode, qubits: tuple[int] | None) -> 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`. - return synthesized_op - return None + 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).""" + return ( + # The fast path is just for Rust-space standard gates (which excludes + # `AnnotatedOperation`). + node.is_standard_gate() + # If it's a controlled gate, we might choose to do funny things to it. + and not node.is_controlled_gate() + # 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 ( + self._instruction_supported(node.name, qubits) + # 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. + or ( + self._equiv_lib is not None + and equivalence.Key(name=node.name, num_qubits=node.num_qubits) + in self._equiv_lib._key_to_node_index + ) + ) + ) + + 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) class DefaultSynthesisClifford(HighLevelSynthesisPlugin): @@ -1137,3 +1254,9 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** return decomposition return None + + +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/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py new file mode 100644 index 000000000000..f3dd34b7df31 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/qubit_tracker.py @@ -0,0 +1,132 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A qubit state tracker for synthesizing operations with auxiliary qubits.""" + +from __future__ import annotations +from collections.abc import Iterable +from dataclasses import dataclass + + +@dataclass +class QubitTracker: + """Track qubits (by global index) and their state. + + The states are distinguished into clean (meaning in state :math:`|0\rangle`) or dirty (an + unknown state). + """ + + # This could in future be extended to track different state types, if necessary. + # However, using sets of integers here is much faster than e.g. storing a dictionary with + # {index: state} entries. + qubits: tuple[int] + clean: set[int] + dirty: set[int] + + def num_clean(self, active_qubits: Iterable[int] | None = None): + """Return the number of clean qubits, not considering the active qubits.""" + # this could be cached if getting the set length becomes a performance bottleneck + return len(self.clean.difference(active_qubits or set())) + + def num_dirty(self, active_qubits: Iterable[int] | None = None): + """Return the number of dirty qubits, not considering the active qubits.""" + return len(self.dirty.difference(active_qubits or set())) + + def borrow(self, num_qubits: int, active_qubits: Iterable[int] | None = None) -> list[int]: + """Get ``num_qubits`` qubits, excluding ``active_qubits``.""" + active_qubits = set(active_qubits or []) + available_qubits = [qubit for qubit in self.qubits if qubit not in active_qubits] + + if num_qubits > (available := len(available_qubits)): + raise RuntimeError(f"Cannot borrow {num_qubits} qubits, only {available} available.") + + # for now, prioritize returning clean qubits + available_clean = [qubit for qubit in available_qubits if qubit in self.clean] + available_dirty = [qubit for qubit in available_qubits if qubit in self.dirty] + + borrowed = available_clean[:num_qubits] + return borrowed + available_dirty[: (num_qubits - len(borrowed))] + + def used(self, qubits: Iterable[int], check: bool = True) -> None: + """Set the state of ``qubits`` to used (i.e. False).""" + qubits = set(qubits) + + if check: + if len(untracked := qubits.difference(self.qubits)) > 0: + raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") + + self.clean -= qubits + self.dirty |= qubits + + def reset(self, qubits: Iterable[int], check: bool = True) -> None: + """Set the state of ``qubits`` to 0 (i.e. True).""" + qubits = set(qubits) + + if check: + if len(untracked := qubits.difference(self.qubits)) > 0: + raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") + + self.clean |= qubits + self.dirty -= qubits + + def drop(self, qubits: Iterable[int], check: bool = True) -> None: + """Drop qubits from the tracker, meaning that they are no longer available.""" + qubits = set(qubits) + + if check: + if len(untracked := qubits.difference(self.qubits)) > 0: + raise ValueError(f"Dropping untracked qubits: {untracked}. Tracker: {self}") + + self.qubits = tuple(qubit for qubit in self.qubits if qubit not in qubits) + self.clean -= qubits + self.dirty -= qubits + + def copy( + self, qubit_map: dict[int, int] | None = None, drop: Iterable[int] | None = None + ) -> "QubitTracker": + """Copy self. + + Args: + qubit_map: If provided, apply the mapping ``{old_qubit: new_qubit}`` to + the qubits in the tracker. Only those old qubits in the mapping will be + part of the new one. + drop: If provided, drop these qubits in the copied tracker. This argument is ignored + if ``qubit_map`` is given, since the qubits can then just be dropped in the map. + """ + if qubit_map is None and drop is not None: + remaining_qubits = [qubit for qubit in self.qubits if qubit not in drop] + qubit_map = dict(zip(remaining_qubits, remaining_qubits)) + + if qubit_map is None: + clean = self.clean.copy() + dirty = self.dirty.copy() + qubits = self.qubits # tuple is immutable, no need to copy + else: + clean, dirty = set(), set() + for old_index, new_index in qubit_map.items(): + if old_index in self.clean: + clean.add(new_index) + elif old_index in self.dirty: + dirty.add(new_index) + else: + raise ValueError(f"Unknown old qubit index: {old_index}. Tracker: {self}") + + qubits = tuple(qubit_map.values()) + + return QubitTracker(qubits, clean=clean, dirty=dirty) + + def __str__(self) -> str: + return ( + f"QubitTracker({len(self.qubits)}, clean: {self.num_clean()}, dirty: {self.num_dirty()})" + + f"\n\tclean: {self.clean}" + + f"\n\tdirty: {self.dirty}" + ) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index d3ba1e60a8eb..4c53bc47b31c 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -42,6 +42,7 @@ def __init__( hls_config=None, init_method=None, optimization_method=None, + qubits_initially_zero=True, ): """Initialize a PassManagerConfig object @@ -84,6 +85,8 @@ def __init__( init_method (str): The plugin name for the init stage plugin to use optimization_method (str): The plugin name for the optimization stage plugin to use. + qubits_initially_zero (bool): Indicates whether the input circuit is + zero-initialized. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -104,6 +107,7 @@ def __init__( self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config self.target = target self.hls_config = hls_config + self.qubits_initially_zero = qubits_initially_zero @classmethod def from_backend(cls, backend, _skip_target=False, **pass_manager_options): @@ -189,5 +193,6 @@ def __str__(self): f"\ttiming_constraints: {self.timing_constraints}\n" f"\tunitary_synthesis_method: {self.unitary_synthesis_method}\n" f"\tunitary_synthesis_plugin_config: {self.unitary_synthesis_plugin_config}\n" + f"\tqubits_initially_zero: {self.qubits_initially_zero}\n" f"\ttarget: {str(self.target).replace(newline, newline_tab)}\n" ) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 7e3e2611546f..d9c272f9d268 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -103,6 +103,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.unitary_synthesis_method, pass_manager_config.unitary_synthesis_plugin_config, pass_manager_config.hls_config, + pass_manager_config.qubits_initially_zero, ) elif optimization_level == 1: init = PassManager() @@ -121,6 +122,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.unitary_synthesis_method, pass_manager_config.unitary_synthesis_plugin_config, pass_manager_config.hls_config, + pass_manager_config.qubits_initially_zero, ) init.append( InverseCancellation( @@ -149,6 +151,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana pass_manager_config.unitary_synthesis_method, pass_manager_config.unitary_synthesis_plugin_config, pass_manager_config.hls_config, + pass_manager_config.qubits_initially_zero, ) init.append(ElidePermutations()) init.append(RemoveDiagonalGatesBeforeMeasure()) @@ -200,6 +203,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, + qubits_initially_zero=pass_manager_config.qubits_initially_zero, ) @@ -217,6 +221,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, + qubits_initially_zero=pass_manager_config.qubits_initially_zero, ) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index b0e9eae57b03..f01639ed115e 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -187,6 +187,7 @@ def generate_unroll_3q( unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, hls_config=None, + qubits_initially_zero=True, ): """Generate an unroll >3q :class:`~qiskit.transpiler.PassManager` @@ -202,8 +203,10 @@ def generate_unroll_3q( configuration, this is plugin specific refer to the specified plugin's documentation for how to use. hls_config (HLSConfig): An optional configuration class to use for - :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass. - Specifies how to synthesize various high-level objects. + :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass. + Specifies how to synthesize various high-level objects. + qubits_initially_zero (bool): Indicates whether the input circuit is + zero-initialized. Returns: PassManager: The unroll 3q or more pass manager @@ -228,6 +231,7 @@ def generate_unroll_3q( equivalence_library=sel, basis_gates=basis_gates, min_qubits=3, + qubits_initially_zero=qubits_initially_zero, ) ) # If there are no target instructions revert to using unroll3qormore so @@ -414,6 +418,7 @@ def generate_translation_passmanager( unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, hls_config=None, + qubits_initially_zero=True, ): """Generate a basis translation :class:`~qiskit.transpiler.PassManager` @@ -439,6 +444,8 @@ def generate_translation_passmanager( hls_config (HLSConfig): An optional configuration class to use for :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass. Specifies how to synthesize various high-level objects. + qubits_initially_zero (bool): Indicates whether the input circuit is + zero-initialized. Returns: PassManager: The basis translation pass manager @@ -466,6 +473,7 @@ def generate_translation_passmanager( use_qubit_indices=True, equivalence_library=sel, basis_gates=basis_gates, + qubits_initially_zero=qubits_initially_zero, ), BasisTranslator(sel, basis_gates, target), ] @@ -490,6 +498,7 @@ def generate_translation_passmanager( use_qubit_indices=True, basis_gates=basis_gates, min_qubits=3, + qubits_initially_zero=qubits_initially_zero, ), Unroll3qOrMore(target=target, basis_gates=basis_gates), Collect2qBlocks(), @@ -512,6 +521,7 @@ def generate_translation_passmanager( target=target, use_qubit_indices=True, basis_gates=basis_gates, + qubits_initially_zero=qubits_initially_zero, ), ] else: diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index a9a9a5e2f029..353ad8c50b15 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -59,6 +59,7 @@ def generate_preset_pass_manager( init_method=None, optimization_method=None, dt=None, + qubits_initially_zero=True, *, _skip_target=False, ): @@ -233,6 +234,8 @@ def generate_preset_pass_manager( plugin is not used. You can see a list of installed plugins by using :func:`~.list_stage_plugins` with ``"optimization"`` for the ``stage_name`` argument. + qubits_initially_zero (bool): Indicates whether the input circuit is + zero-initialized. Returns: StagedPassManager: The preset pass manager for the given options @@ -376,6 +379,7 @@ def generate_preset_pass_manager( "hls_config": hls_config, "init_method": init_method, "optimization_method": optimization_method, + "qubits_initially_zero": qubits_initially_zero, } if backend is not None: diff --git a/releasenotes/notes/hls-with-ancillas-d6792b41dfcf4aac.yaml b/releasenotes/notes/hls-with-ancillas-d6792b41dfcf4aac.yaml new file mode 100644 index 000000000000..b70a032733c4 --- /dev/null +++ b/releasenotes/notes/hls-with-ancillas-d6792b41dfcf4aac.yaml @@ -0,0 +1,18 @@ +--- +features_transpiler: + - | + A new argument ``qubits_initially_zero`` has been added to :func:`qiskit.compiler.transpile`, + :func:`.generate_preset_pass_manager`, and to :class:`~.PassManagerConfig`. + If set to ``True``, the qubits are assumed to be initially in the state :math:`|0\rangle`, + potentially allowing additional optimization opportunities for individual transpiler passes. + - | + The constructor for :class:`.HighLevelSynthesis` transpiler pass now accepts an + additional argument ``qubits_initially_zero``. If set to ``True``, the pass assumes that the + qubits are initially in the state :math:`|0\rangle`. In addition, the pass keeps track of + clean and dirty auxiliary qubits throughout the run, and passes this information to plugins + via kwargs ``num_clean_ancillas`` and ``num_dirty_ancillas``. +upgrade_transpiler: + - | + The :func:`~qiskit.compiler.transpile` now assumes that the qubits are initially in the state + :math:`|0\rangle`. To avoid this assumption, one can set the argument ``qubits_initially_zero`` + to ``False``. diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index d1ea21cde544..97c0b58bb377 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -28,6 +28,7 @@ Parameter, Operation, EquivalenceLibrary, + Delay, ) from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( @@ -42,6 +43,7 @@ CU3Gate, CU1Gate, QFTGate, + IGate, ) from qiskit.circuit.library.generalized_gates import LinearFunction from qiskit.quantum_info import Clifford @@ -220,6 +222,39 @@ def method(self, op_name, method_name): return self.plugins[plugin_name]() +class MockPlugin(HighLevelSynthesisPlugin): + """A mock HLS using auxiliary qubits.""" + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run a mock synthesis for high_level_object being anything with a num_qubits property. + + Replaces the high_level_objects by a layer of X gates, applies S gates on clean + ancillas and T gates on dirty ancillas. + """ + + num_action_qubits = high_level_object.num_qubits + num_clean = options["num_clean_ancillas"] + num_dirty = options["num_dirty_ancillas"] + num_qubits = num_action_qubits + num_clean + num_dirty + decomposition = QuantumCircuit(num_qubits) + decomposition.x(range(num_action_qubits)) + if num_clean > 0: + decomposition.s(range(num_action_qubits, num_action_qubits + num_clean)) + if num_dirty > 0: + decomposition.t(range(num_action_qubits + num_clean, num_qubits)) + + return decomposition + + +class EmptyPlugin(HighLevelSynthesisPlugin): + """A mock plugin returning None (i.e. a failed synthesis).""" + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Elaborate code to return None :)""" + return None + + +@ddt class TestHighLevelSynthesisInterface(QiskitTestCase): """Tests for the synthesis plugin interface.""" @@ -481,7 +516,7 @@ def test_target_gets_passed_to_plugins(self): [ HighLevelSynthesis( hls_config=hls_config, - target=GenericBackendV2(num_qubits=5, basis_gates=["u", "cx"]).target, + target=GenericBackendV2(num_qubits=5, basis_gates=["u", "cx", "id"]).target, ) ] ) @@ -514,6 +549,111 @@ def test_qubits_get_passed_to_plugins(self): # plugin should see qubits and complete without errors. pm_use_qubits_true.run(qc) + def test_ancilla_arguments(self): + """Test ancillas are correctly labelled.""" + gate = Gate(name="duckling", num_qubits=5, params=[]) + hls_config = HLSConfig(duckling=[MockPlugin()]) + + qc = QuantumCircuit(10) + qc.h([0, 8, 9]) # the two last H gates yield two dirty ancillas + qc.barrier() + qc.append(gate, range(gate.num_qubits)) + + pm = PassManager([HighLevelSynthesis(hls_config=hls_config)]) + + synthesized = pm.run(qc) + + count = synthesized.count_ops() + self.assertEqual(count.get("x", 0), gate.num_qubits) # gate qubits + self.assertEqual(count.get("s", 0), qc.num_qubits - gate.num_qubits - 2) # clean + self.assertEqual(count.get("t", 0), 2) # dirty + + def test_ancilla_noop(self): + """Test ancillas states are not affected by no-ops.""" + gate = Gate(name="duckling", num_qubits=1, params=[]) + hls_config = HLSConfig(duckling=[MockPlugin()]) + pm = PassManager([HighLevelSynthesis(hls_config)]) + + noops = [Delay(100), IGate()] + for noop in noops: + qc = QuantumCircuit(2) + qc.append(noop, [1]) # this noop should still yield a clean ancilla + qc.barrier() + qc.append(gate, [0]) + + synthesized = pm.run(qc) + count = synthesized.count_ops() + with self.subTest(noop=noop): + self.assertEqual(count.get("x", 0), gate.num_qubits) # gate qubits + self.assertEqual(count.get("s", 0), 1) # clean ancilla + self.assertEqual(count.get("t", 0), 0) # dirty ancilla + + @data(True, False) + def test_ancilla_reset(self, reset): + """Test ancillas are correctly freed after a reset operation.""" + gate = Gate(name="duckling", num_qubits=1, params=[]) + hls_config = HLSConfig(duckling=[MockPlugin()]) + pm = PassManager([HighLevelSynthesis(hls_config)]) + + qc = QuantumCircuit(2) + qc.h(1) + if reset: + qc.reset(1) # the reset frees the ancilla qubit again + qc.barrier() + qc.append(gate, [0]) + + synthesized = pm.run(qc) + count = synthesized.count_ops() + + expected_clean = 1 if reset else 0 + expected_dirty = 1 - expected_clean + + self.assertEqual(count.get("x", 0), gate.num_qubits) # gate qubits + self.assertEqual(count.get("s", 0), expected_clean) # clean ancilla + self.assertEqual(count.get("t", 0), expected_dirty) # clean ancilla + + def test_ancilla_state_maintained(self): + """Test ancillas states are still dirty/clean after they've been used.""" + gate = Gate(name="duckling", num_qubits=1, params=[]) + hls_config = HLSConfig(duckling=[MockPlugin()]) + pm = PassManager([HighLevelSynthesis(hls_config)]) + + qc = QuantumCircuit(3) + qc.h(2) # the final ancilla is dirty + qc.barrier() + qc.append(gate, [0]) + qc.append(gate, [0]) + + # the ancilla states should be unchanged after the synthesis, i.e. qubit 1 is always + # clean (S gate) and qubit 2 is always dirty (T gate) + ref = QuantumCircuit(3) + ref.h(2) + ref.barrier() + for _ in range(2): + ref.x(0) + ref.s(1) + ref.t(2) + + self.assertEqual(ref, pm.run(qc)) + + def test_synth_fails_definition_exists(self): + """Test the case that a synthesis fails but the operation can be unrolled.""" + + circuit = QuantumCircuit(1) + circuit.ry(0.2, 0) + + config = HLSConfig(ry=[EmptyPlugin()]) + hls = HighLevelSynthesis(hls_config=config) + + with self.subTest("nothing happened w/o basis gates"): + out = hls(circuit) + self.assertEqual(out, circuit) + + hls = HighLevelSynthesis(hls_config=config, basis_gates=["u"]) + with self.subTest("unrolled w/ basis gates"): + out = hls(circuit) + self.assertEqual(out.count_ops(), {"u": 1}) + class TestPMHSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the PMHSynthesisLinearFunction plugin for synthesizing linear functions.""" @@ -1743,6 +1883,20 @@ def test_unrolling_parameterized_composite_gates(self): self.assertEqual(circuit_to_dag(expected), out_dag) + def test_unroll_with_clbit(self): + """Test unrolling a custom definition that has qubits and clbits.""" + block = QuantumCircuit(1, 1) + block.h(0) + block.measure(0, 0) + + circuit = QuantumCircuit(1, 1) + circuit.append(block.to_instruction(), [0], [0]) + + hls = HighLevelSynthesis(basis_gates=["h", "measure"]) + out = hls(circuit) + + self.assertEqual(block, out) + class TestGate(Gate): """Mock one qubit zero param gate.""" diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index ebac6a410b7f..341da357d37a 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -140,6 +140,7 @@ def test_str(self): \ttiming_constraints: None \tunitary_synthesis_method: default \tunitary_synthesis_plugin_config: None +\tqubits_initially_zero: True \ttarget: Target: Basic Target \tNumber of qubits: None \tInstructions: