diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 9cc763cbd35c..d44051745a89 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -22,7 +22,9 @@ use pyo3::{intern, IntoPy, PyObject, PyResult}; use smallvec::SmallVec; -use crate::imports::{CONTROL_FLOW_OP, GATE, INSTRUCTION, OPERATION, WARNINGS_WARN}; +use crate::imports::{ + CONTROLLED_GATE, CONTROL_FLOW_OP, GATE, INSTRUCTION, OPERATION, WARNINGS_WARN, +}; use crate::operations::{ Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, }; @@ -271,6 +273,19 @@ impl CircuitInstruction { self.operation.try_standard_gate().is_some() } + /// Is the :class:`.Operation` contained in this instruction a subclass of + /// :class:`.ControlledGate`? + pub fn is_controlled_gate(&self, py: Python) -> PyResult { + match self.operation.view() { + OperationRef::Standard(standard) => Ok(standard.num_ctrl_qubits() != 0), + OperationRef::Gate(gate) => gate + .gate + .bind(py) + .is_instance(CONTROLLED_GATE.get_bound(py)), + _ => Ok(false), + } + } + /// Is the :class:`.Operation` contained in this node a directive? pub fn is_directive(&self) -> bool { self.op().directive() diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 4b6c1b7c0bf0..73983c35e2e8 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -334,6 +334,11 @@ impl DAGOpNode { self.instruction.is_standard_gate() } + /// Is the :class:`.Operation` contained in this node a subclass of :class:`.ControlledGate`? + pub fn is_controlled_gate(&self, py: Python) -> PyResult { + self.instruction.is_controlled_gate(py) + } + /// Is the :class:`.Operation` contained in this node a directive? pub fn is_directive(&self) -> bool { self.instruction.is_directive() diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index d8ca3e35c524..6974b1cce06c 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -159,7 +159,10 @@ QFTSynthesisLine """ -from typing import Optional, Union, List, Tuple, Callable +from __future__ import annotations + +import typing +from typing import Optional, Union, List, Tuple, Callable, Sequence import numpy as np import rustworkx as rx @@ -168,7 +171,7 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit import ControlledGate, EquivalenceLibrary +from qiskit.circuit import ControlledGate, EquivalenceLibrary, equivalence from qiskit.circuit.library import LinearFunction from qiskit.transpiler.passes.utils import control_flow from qiskit.transpiler.target import Target @@ -210,6 +213,9 @@ from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin +if typing.TYPE_CHECKING: + from qiskit.dagcircuit import DAGOpNode + class HLSConfig: """The high-level-synthesis config allows to specify a list of "methods" used by @@ -396,6 +402,8 @@ def __init__( if not self._top_level_only and (self._target is None or self._target.num_qubits is None): basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) + else: + self._device_insts = set() def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the HighLevelSynthesis pass on `dag`. @@ -429,7 +437,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None ) - if self._definitely_skip_node(node): + if self._definitely_skip_node(node, qubits): continue decomposition, modified = self._recursively_handle_op(node.op, qubits) @@ -448,16 +456,42 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return dag - def _definitely_skip_node(self, node) -> bool: + 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). + 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).""" - # In general, Rust-space standard gates aren't going to need synthesising (but check to be - # sure). - return node.is_standard_gate() and not self._methods_to_try(node.name) + 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: 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 @@ -507,17 +541,9 @@ def _recursively_handle_op( # or is in equivalence library controlled_gate_open_ctrl = isinstance(op, ControlledGate) and op._open_ctrl if not controlled_gate_open_ctrl: - qargs = tuple(qubits) if qubits is not None else None - # include path for when target exists but target.num_qubits is None (BasicSimulator) - inst_supported = ( - self._target.instruction_supported( - operation_name=op.name, - qargs=qargs, - ) - if self._target is not None and self._target.num_qubits is not None - else op.name in self._device_insts - ) - if inst_supported or (self._equiv_lib is not None and self._equiv_lib.has_entry(op)): + if self._instruction_supported(op.name, qubits) or ( + self._equiv_lib is not None and self._equiv_lib.has_entry(op) + ): return op, False try: diff --git a/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml b/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml index 79fcdb26f143..c7f63a45f8cc 100644 --- a/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml +++ b/releasenotes/notes/avoid-op-creation-804c0bed6c408911.yaml @@ -6,6 +6,7 @@ features_circuits: These methods are: * :meth:`.CircuitInstruction.is_standard_gate` and :meth:`.DAGOpNode.is_standard_gate`, + * :meth:`.CircuitInstruction.is_controlled_gate` and :meth:`.DAGOpNode.is_controlled_gate`, * :meth:`.CircuitInstruction.is_directive` and :meth:`.DAGOpNode.is_directive`, * :meth:`.CircuitInstruction.is_control_flow` and :meth:`.DAGOpNode.is_control_flow`, and * :meth:`.CircuitInstruction.is_parameterized` and :meth:`.DAGOpNode.is_parameterized`.