diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 07f4579a4cd3..58e657a574de 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -18,7 +18,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, PackedInstruction, }; -use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::imports::{BUILTIN_LIST, DEEPCOPY, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationType, Param, StandardGate}; use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; @@ -487,20 +487,17 @@ impl CircuitData { res.param_table.clone_from(&self.param_table); if deepcopy { - let deepcopy = py - .import_bound(intern!(py, "copy"))? - .getattr(intern!(py, "deepcopy"))?; for inst in &mut res.data { match &mut inst.op { OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { - op.gate = deepcopy.call1((&op.gate,))?.unbind(); + op.gate = DEEPCOPY.get_bound(py).call1((&op.gate,))?.unbind(); } OperationType::Instruction(ref mut op) => { - op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + op.instruction = DEEPCOPY.get_bound(py).call1((&op.instruction,))?.unbind(); } OperationType::Operation(ref mut op) => { - op.operation = deepcopy.call1((&op.operation,))?.unbind(); + op.operation = DEEPCOPY.get_bound(py).call1((&op.operation,))?.unbind(); } }; #[cfg(feature = "cache_pygates")] diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 74302b526d51..6e822ba422c8 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -13,6 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::cell::RefCell; +use numpy::IntoPyArray; use pyo3::basic::CompareOp; use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; use pyo3::prelude::*; @@ -25,7 +26,9 @@ use crate::imports::{ SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, }; use crate::interner::Index; -use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; +use crate::operations::{ + Operation, OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, +}; /// These are extra mutable attributes for a circuit instruction's state. In general we don't /// typically deal with this in rust space and the majority of the time they're not used in Python @@ -407,6 +410,56 @@ impl CircuitInstruction { }) } + #[getter] + fn _raw_op(&self, py: Python) -> PyObject { + self.operation.clone().into_py(py) + } + + /// Returns the Instruction name corresponding to the op for this node + #[getter] + fn get_name(&self, py: Python) -> PyObject { + self.operation.name().to_object(py) + } + + #[getter] + fn get_params(&self, py: Python) -> PyObject { + self.params.to_object(py) + } + + #[getter] + fn matrix(&self, py: Python) -> Option { + let matrix = self.operation.matrix(&self.params); + matrix.map(|mat| mat.into_pyarray_bound(py).into()) + } + + #[getter] + fn label(&self) -> Option<&str> { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_deref()) + } + + #[getter] + fn condition(&self, py: Python) -> Option { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn duration(&self, py: Python) -> Option { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn unit(&self) -> Option<&str> { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.unit.as_deref()) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c8b6a4c8b082..ffd7920a36fd 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -15,9 +15,11 @@ use crate::circuit_instruction::{ ExtraInstructionAttributes, }; use crate::operations::Operation; +use numpy::IntoPyArray; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; -use pyo3::{intern, PyObject, PyResult}; +use pyo3::{intern, IntoPy, PyObject, PyResult}; +use smallvec::smallvec; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] @@ -70,12 +72,19 @@ pub struct DAGOpNode { #[pymethods] impl DAGOpNode { + #[allow(clippy::too_many_arguments)] #[new] + #[pyo3(signature = (op, qargs=None, cargs=None, params=smallvec![], label=None, duration=None, unit=None, condition=None, dag=None))] fn new( py: Python, - op: PyObject, + op: crate::circuit_instruction::OperationInput, qargs: Option<&Bound>, cargs: Option<&Bound>, + params: smallvec::SmallVec<[crate::operations::Param; 3]>, + label: Option, + duration: Option, + unit: Option, + condition: Option, dag: Option<&Bound>, ) -> PyResult<(Self, DAGNode)> { let qargs = @@ -110,34 +119,16 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; - let res = convert_py_to_operation_type(py, op.clone_ref(py))?; - let extra_attrs = if res.label.is_some() - || res.duration.is_some() - || res.unit.is_some() - || res.condition.is_some() - { - Some(Box::new(ExtraInstructionAttributes { - label: res.label, - duration: res.duration, - unit: res.unit, - condition: res.condition, - })) - } else { - None - }; + let mut instruction = CircuitInstruction::py_new( + py, op, None, None, params, label, duration, unit, condition, + )?; + instruction.qubits = qargs.into(); + instruction.clbits = cargs.into(); Ok(( DAGOpNode { - instruction: CircuitInstruction { - operation: res.operation, - qubits: qargs.unbind(), - clbits: cargs.unbind(), - params: res.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(op), - }, + instruction, sort_key: sort_key.unbind(), }, DAGNode { _node_id: -1 }, @@ -219,6 +210,77 @@ impl DAGOpNode { self.instruction.operation.name().to_object(py) } + #[getter] + fn get_params(&self, py: Python) -> PyObject { + self.instruction.params.to_object(py) + } + + #[getter] + fn matrix(&self, py: Python) -> Option { + let matrix = self.instruction.operation.matrix(&self.instruction.params); + matrix.map(|mat| mat.into_pyarray_bound(py).into()) + } + + #[getter] + fn label(&self) -> Option<&str> { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_deref()) + } + + #[getter] + fn condition(&self, py: Python) -> Option { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn duration(&self, py: Python) -> Option { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn unit(&self) -> Option<&str> { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.unit.as_deref()) + } + + #[setter] + fn set_label(&mut self, val: Option) { + match self.instruction.extra_attrs.as_mut() { + Some(attrs) => attrs.label = val, + None => { + if val.is_some() { + self.instruction.extra_attrs = Some(Box::new( + crate::circuit_instruction::ExtraInstructionAttributes { + label: val, + duration: None, + unit: None, + condition: None, + }, + )) + } + } + }; + if let Some(attrs) = &self.instruction.extra_attrs { + if attrs.label.is_none() + && attrs.duration.is_none() + && attrs.unit.is_none() + && attrs.condition.is_none() + { + self.instruction.extra_attrs = None; + } + } + } + /// Sets the Instruction name corresponding to the op for this node #[setter] fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { @@ -229,6 +291,11 @@ impl DAGOpNode { Ok(()) } + #[getter] + fn _raw_op(&self, py: Python) -> PyObject { + self.instruction.operation.clone().into_py(py) + } + /// Returns a representation of the DAGOpNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!( diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 530e635c94f1..e2a1ca542453 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -71,6 +71,7 @@ pub static SINGLETON_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 85192b63dbd7..3fe808876a70 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -13,7 +13,7 @@ use std::f64::consts::PI; use crate::circuit_data::CircuitData; -use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::imports::{DEEPCOPY, PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; use crate::{gate_matrix, Qubit}; use ndarray::{aview2, Array2}; @@ -35,6 +35,17 @@ pub enum OperationType { Operation(PyOperation), } +impl IntoPy for OperationType { + fn into_py(self, py: Python) -> PyObject { + match self { + Self::Standard(gate) => gate.into_py(py), + Self::Instruction(inst) => inst.into_py(py), + Self::Gate(gate) => gate.into_py(py), + Self::Operation(op) => op.into_py(py), + } + } +} + impl Operation for OperationType { fn name(&self) -> &str { match self { @@ -1174,6 +1185,16 @@ impl PyInstruction { instruction, } } + + fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { + Ok(PyInstruction { + qubits: self.qubits, + clbits: self.clbits, + params: self.params, + op_name: self.op_name.clone(), + instruction: DEEPCOPY.get_bound(py).call1((&self.instruction,))?.unbind(), + }) + } } impl Operation for PyInstruction { @@ -1253,6 +1274,16 @@ impl PyGate { gate, } } + + fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { + Ok(PyGate { + qubits: self.qubits, + clbits: self.clbits, + params: self.params, + op_name: self.op_name.clone(), + gate: DEEPCOPY.get_bound(py).call1((&self.gate,))?.unbind(), + }) + } } impl Operation for PyGate { @@ -1345,6 +1376,16 @@ impl PyOperation { operation, } } + + fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { + Ok(PyOperation { + qubits: self.qubits, + clbits: self.clbits, + params: self.params, + op_name: self.op_name.clone(), + operation: DEEPCOPY.get_bound(py).call1((&self.operation,))?.unbind(), + }) + } } impl Operation for PyOperation { diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index b2c1df2a037b..88d9c72f1d61 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -13,7 +13,8 @@ """Helper function for converting a circuit to a dag""" import copy -from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode +from qiskit._accelerate.circuit import StandardGate def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): @@ -93,10 +94,24 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord dagcircuit.add_creg(register) for instruction in circuit.data: - op = instruction.operation - if copy_operations: - op = copy.deepcopy(op) - dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False) + if not isinstance(instruction._raw_op, StandardGate): + op = instruction.operation + if copy_operations: + op = copy.deepcopy(op) + dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False) + else: + node = DAGOpNode( + instruction._raw_op, + qargs=instruction.qubits, + cargs=instruction.clbits, + params=instruction.params, + label=instruction.label, + duration=instruction.duration, + unit=instruction.unit, + condition=instruction.condition, + dag=dagcircuit, + ) + dagcircuit._apply_op_node_back(node) dagcircuit.duration = circuit.duration dagcircuit.unit = circuit.unit diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index ede026c247c9..3667c2183eae 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -14,6 +14,7 @@ import copy from qiskit.circuit import QuantumCircuit, CircuitInstruction +from qiskit._accelerate.circuit import StandardGate def dag_to_circuit(dag, copy_operations=True): @@ -71,10 +72,24 @@ def dag_to_circuit(dag, copy_operations=True): circuit.calibrations = dag.calibrations for node in dag.topological_op_nodes(): - op = node.op - if copy_operations: - op = copy.deepcopy(op) - circuit._append(CircuitInstruction(op, node.qargs, node.cargs)) + if not isinstance(node._raw_op, StandardGate): + op = node.op + if copy_operations: + op = copy.deepcopy(op) + circuit._append(CircuitInstruction(op, node.qargs, node.cargs)) + else: + circuit._append( + CircuitInstruction( + node._raw_op, + node.qargs, + node.cargs, + params=node.params, + label=node.label, + duration=node.duration, + unit=node.unit, + condition=node.condition, + ) + ) circuit.duration = dag.duration circuit.unit = dag.unit diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index d14340a8cb9b..0213d242097f 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -717,6 +717,27 @@ def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): return target_dag + def _apply_op_node_back(self, node: DAGOpNode): + additional = () + if _may_have_additional_wires(node): + # This is the slow path; most of the time, this won't happen. + additional = set(_additional_wires(node)).difference(node.cargs) + + node._node_id = self._multi_graph.add_node(node) + self._increment_op(node) + + # Add new in-edges from predecessors of the output nodes to the + # operation node while deleting the old in-edges of the output nodes + # and adding new edges from the operation node to each output node + self._multi_graph.insert_node_on_in_edges_multiple( + node._node_id, + [ + self.output_map[bit]._node_id + for bits in (node.qargs, node.cargs, additional) + for bit in bits + ], + ) + def apply_operation_back( self, op: Operation, diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 7db48d6d1395..ab7c5e04649f 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -32,17 +32,17 @@ from qiskit.transpiler import CouplingMap, Target from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError -from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import _possible_decomposers from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, - GATE_NAME_MAP, ) from qiskit.quantum_info import Operator -from qiskit.circuit import ControlFlowOp, Gate, Parameter +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit.circuit import Gate, Parameter from qiskit.circuit.library.standard_gates import ( iSwapGate, CXGate, @@ -50,6 +50,17 @@ RXXGate, RZXGate, ECRGate, + RXGate, + SXGate, + XGate, + RZGate, + UGate, + PhaseGate, + U1Gate, + U2Gate, + U3Gate, + RYGate, + RGate, ) from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( @@ -60,6 +71,22 @@ from qiskit.exceptions import QiskitError +GATE_NAME_MAP = { + "cx": CXGate._standard_gate, + "rx": RXGate._standard_gate, + "sx": SXGate._standard_gate, + "x": XGate._standard_gate, + "rz": RZGate._standard_gate, + "u": UGate._standard_gate, + "p": PhaseGate._standard_gate, + "u1": U1Gate._standard_gate, + "u2": U2Gate._standard_gate, + "u3": U3Gate._standard_gate, + "ry": RYGate._standard_gate, + "r": RGate._standard_gate, +} + + KAK_GATE_NAMES = { "cx": CXGate(), "cz": CZGate(), @@ -479,7 +506,9 @@ def _run_main_loop( self, dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs ): """Inner loop for the optimizer, after all DAG-independent set-up has been completed.""" - for node in dag.op_nodes(ControlFlowOp): + for node in dag.op_nodes(): + if node.name not in CONTROL_FLOW_OP_NAMES: + continue node.op = node.op.replace_blocks( [ dag_to_circuit( @@ -502,9 +531,9 @@ def _run_main_loop( out_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): - if node.op.name == "unitary" and len(node.qargs) >= self._min_qubits: + if node.name == "unitary" and len(node.qargs) >= self._min_qubits: synth_dag = None - unitary = node.op.to_matrix() + unitary = node.matrix n_qubits = len(node.qargs) if ( plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits @@ -519,35 +548,41 @@ def _run_main_loop( ) synth_dag = method.run(unitary, **kwargs) if synth_dag is None: - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + out_dag._apply_op_node_back(node) continue if isinstance(synth_dag, DAGCircuit): qubit_map = dict(zip(synth_dag.qubits, node.qargs)) for node in synth_dag.topological_op_nodes(): - out_dag.apply_operation_back( - node.op, (qubit_map[x] for x in node.qargs), check=False - ) + node.qargs = tuple(qubit_map[x] for x in node.qargs) + out_dag._apply_op_node_back(node) out_dag.global_phase += synth_dag.global_phase else: node_list, global_phase, gate = synth_dag qubits = node.qargs + user_gate_node = DAGOpNode(gate) for ( op_name, params, qargs, ) in node_list: if op_name == "USER_GATE": - op = gate + node = DAGOpNode( + user_gate_node._raw_op, + params=user_gate_node.params, + qargs=tuple(qubits[x] for x in qargs), + dag=out_dag, + ) else: - op = GATE_NAME_MAP[op_name](*params) - out_dag.apply_operation_back( - op, - (qubits[x] for x in qargs), - check=False, - ) + node = DAGOpNode( + GATE_NAME_MAP[op_name], + params=params, + qargs=tuple(qubits[x] for x in qargs), + dag=out_dag, + ) + out_dag._apply_op_node_back(node) out_dag.global_phase += global_phase else: - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + out_dag._apply_op_node_back(node) return out_dag @@ -1008,5 +1043,6 @@ def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): flip_bits = out_dag.qubits[::-1] for node in synth_circ.topological_op_nodes(): qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - out_dag.apply_operation_back(node.op, qubits, check=False) + node = DAGOpNode(node._raw_op, qargs=qubits, params=node.params) + out_dag._apply_op_node_back(node) return out_dag diff --git a/qiskit/transpiler/passes/utils/check_gate_direction.py b/qiskit/transpiler/passes/utils/check_gate_direction.py index 1ddfd40124b5..e797be95c4a1 100644 --- a/qiskit/transpiler/passes/utils/check_gate_direction.py +++ b/qiskit/transpiler/passes/utils/check_gate_direction.py @@ -12,7 +12,7 @@ """Check if the gates follow the right direction with respect to the coupling map.""" -from qiskit.circuit import ControlFlowOp +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import AnalysisPass @@ -39,7 +39,7 @@ def _coupling_map_visit(self, dag, wire_map, edges=None): edges = self.coupling_map.get_edges() # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: inner_wire_map = { inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) @@ -57,7 +57,7 @@ def _coupling_map_visit(self, dag, wire_map, edges=None): def _target_visit(self, dag, wire_map): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: inner_wire_map = { inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) @@ -65,7 +65,7 @@ def _target_visit(self, dag, wire_map): if not self._target_visit(circuit_to_dag(block), inner_wire_map): return False elif len(node.qargs) == 2 and not self.target.instruction_supported( - node.op.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) + node.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) ): return False return True