diff --git a/crates/accelerate/src/basis/basis_translator/basis_search.rs b/crates/accelerate/src/basis/basis_translator/basis_search.rs index 2810765db741..4686ba9c4c3f 100644 --- a/crates/accelerate/src/basis/basis_translator/basis_search.rs +++ b/crates/accelerate/src/basis/basis_translator/basis_search.rs @@ -13,7 +13,6 @@ use std::cell::RefCell; use hashbrown::{HashMap, HashSet}; -use pyo3::prelude::*; use crate::equivalence::{EdgeData, Equivalence, EquivalenceLibrary, Key, NodeData}; use qiskit_circuit::operations::Operation; @@ -23,28 +22,6 @@ use rustworkx_core::traversal::{dijkstra_search, DijkstraEvent}; use super::compose_transforms::{BasisTransformIn, GateIdentifier}; -/// Search for a set of transformations from source_basis to target_basis. -/// Args: -/// equiv_lib (EquivalenceLibrary): Source of valid translations -/// source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis. -/// target_basis (Set[gate_name: str]): Target basis. -/// -/// Returns: -/// Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate, -/// equiv_params, equiv_circuit) tuples tuples which, if applied in order -/// will map from source_basis to target_basis. Returns None if no path -/// was found. -#[pyfunction] -#[pyo3(name = "basis_search")] -pub(crate) fn py_basis_search( - py: Python, - equiv_lib: &mut EquivalenceLibrary, - source_basis: HashSet, - target_basis: HashSet, -) -> PyObject { - basis_search(equiv_lib, &source_basis, &target_basis).into_py(py) -} - type BasisTransforms = Vec<(GateIdentifier, BasisTransformIn)>; /// Search for a set of transformations from source_basis to target_basis. /// diff --git a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs index e4498af0f8a5..b1366c30bf2f 100644 --- a/crates/accelerate/src/basis/basis_translator/compose_transforms.rs +++ b/crates/accelerate/src/basis/basis_translator/compose_transforms.rs @@ -30,16 +30,6 @@ pub type GateIdentifier = (String, u32); pub type BasisTransformIn = (SmallVec<[Param; 3]>, CircuitFromPython); pub type BasisTransformOut = (SmallVec<[Param; 3]>, DAGCircuit); -#[pyfunction(name = "compose_transforms")] -pub(super) fn py_compose_transforms( - py: Python, - basis_transforms: Vec<(GateIdentifier, BasisTransformIn)>, - source_basis: HashSet, - source_dag: &DAGCircuit, -) -> PyResult> { - compose_transforms(py, &basis_transforms, &source_basis, source_dag) -} - pub(super) fn compose_transforms<'a>( py: Python, basis_transforms: &'a [(GateIdentifier, BasisTransformIn)], diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 18970065267c..727d041a1947 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -10,14 +10,914 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use compose_transforms::BasisTransformIn; +use compose_transforms::BasisTransformOut; +use compose_transforms::GateIdentifier; + +use basis_search::basis_search; +use compose_transforms::compose_transforms; +use hashbrown::{HashMap, HashSet}; +use itertools::Itertools; +use pyo3::intern; use pyo3::prelude::*; -pub mod basis_search; +mod basis_search; mod compose_transforms; +use pyo3::types::{IntoPyDict, PyComplex, PyDict, PyTuple}; +use pyo3::PyTypeInfo; +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::converters::circuit_to_dag; +use qiskit_circuit::imports::DAG_TO_CIRCUIT; +use qiskit_circuit::imports::PARAMETER_EXPRESSION; +use qiskit_circuit::operations::Param; +use qiskit_circuit::packed_instruction::PackedInstruction; +use qiskit_circuit::{ + circuit_data::CircuitData, + dag_circuit::{DAGCircuit, NodeType}, + operations::{Operation, OperationRef}, +}; +use qiskit_circuit::{Clbit, Qubit}; +use smallvec::SmallVec; + +use crate::equivalence::EquivalenceLibrary; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::exceptions::TranspilerError; +use crate::target_transpiler::{Qargs, Target}; + +#[pyclass( + name = "CoreBasisTranslator", + module = "qiskit._accelerate.basis.basis_translator" +)] +pub struct BasisTranslator { + equiv_lib: EquivalenceLibrary, + target_basis: Option>, + target: Option, + non_global_operations: Option>, + qargs_with_non_global_operation: HashMap, HashSet>, + min_qubits: usize, +} + +type InstMap = HashMap; +type ExtraInstructionMap<'a> = HashMap<&'a Option, InstMap>; + +#[pymethods] +impl BasisTranslator { + #[new] + fn new( + equiv_lib: EquivalenceLibrary, + min_qubits: usize, + target_basis: Option>, + mut target: Option, + ) -> Self { + let mut non_global_operations = None; + let mut qargs_with_non_global_operation: HashMap, HashSet> = + HashMap::default(); + + if let Some(target) = target.as_mut() { + let non_global_from_target: HashSet = target + .get_non_global_operation_names(false) + .unwrap_or_default() + .iter() + .cloned() + .collect(); + for gate in &non_global_from_target { + for qarg in target[gate].keys() { + qargs_with_non_global_operation + .entry(qarg.cloned()) + .and_modify(|set| { + set.insert(gate.clone()); + }) + .or_insert(HashSet::from_iter([gate.clone()])); + } + } + non_global_operations = Some(non_global_from_target); + } + Self { + equiv_lib, + target_basis, + target, + non_global_operations, + qargs_with_non_global_operation, + min_qubits, + } + } + + fn run(&mut self, py: Python<'_>, dag: DAGCircuit) -> PyResult { + if self.target_basis.is_none() && self.target.is_none() { + return Ok(dag); + } + + let basic_instrs: HashSet; + let mut source_basis: HashSet = HashSet::default(); + let mut target_basis: HashSet; + let mut qargs_local_source_basis: HashMap, HashSet> = + HashMap::default(); + if let Some(target) = self.target.as_ref() { + basic_instrs = ["barrier", "snapshot", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + let non_global_str: HashSet<&str> = + if let Some(operations) = self.non_global_operations.as_ref() { + operations.iter().map(|x| x.as_str()).collect() + } else { + HashSet::default() + }; + let target_keys = target.keys().collect::>(); + target_basis = target_keys + .difference(&non_global_str) + .map(|x| x.to_string()) + .collect(); + self.extract_basis_target(py, &dag, &mut source_basis, &mut qargs_local_source_basis)?; + } else { + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + source_basis = self.extract_basis(py, &dag)?; + target_basis = self.target_basis.clone().unwrap(); + } + target_basis = target_basis + .union(&basic_instrs) + .map(|x| x.to_string()) + .collect(); + // If the source basis is a subset of the target basis and we have no circuit + // instructions on qargs that have non-global operations there is nothing to + // translate and we can exit early. + let source_basis_names: HashSet = + source_basis.iter().map(|x| x.0.clone()).collect(); + if source_basis_names.is_subset(&target_basis) && qargs_local_source_basis.is_empty() { + return Ok(dag); + } + let basis_transforms = basis_search(&mut self.equiv_lib, &source_basis, &target_basis); + let mut qarg_local_basis_transforms: HashMap< + Option, + Vec<(GateIdentifier, BasisTransformIn)>, + > = HashMap::default(); + for (qarg, local_source_basis) in qargs_local_source_basis.iter() { + // For any multiqubit operation that contains a subset of qubits that + // has a non-local operation, include that non-local operation in the + // search. This matches with the check we did above to include those + // subset non-local operations in the check here. + let mut expanded_target = target_basis.clone(); + if qarg.as_ref().is_some_and(|qarg| qarg.len() > 1) { + let qarg_as_set: HashSet = + HashSet::from_iter(qarg.as_ref().unwrap().iter().copied()); + for (non_local_qarg, local_basis) in self.qargs_with_non_global_operation.iter() { + if let Some(non_local_qarg) = non_local_qarg { + let non_local_qarg_as_set = + HashSet::from_iter(non_local_qarg.iter().copied()); + if qarg_as_set.is_superset(&non_local_qarg_as_set) { + expanded_target = expanded_target.union(local_basis).cloned().collect(); + } + } + } + } else { + expanded_target = expanded_target + .union(&self.qargs_with_non_global_operation[qarg]) + .cloned() + .collect(); + } + let local_basis_transforms = + basis_search(&mut self.equiv_lib, local_source_basis, &expanded_target); + if let Some(local_basis_transforms) = local_basis_transforms { + qarg_local_basis_transforms.insert(qarg.clone(), local_basis_transforms); + } else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) target \ + basis: {:?}. This likely means the target basis is not universal \ + or there are additional equivalence rules needed in the EquivalenceLibrary being \ + used. For more details on this error see: \ + https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes.\ + BasisTranslator#translation-errors", + local_source_basis + .iter() + .map(|x| x.0.as_str()) + .collect_vec(), + &expanded_target + ))); + } + } + + let Some(basis_transforms) = basis_transforms else { + return Err(TranspilerError::new_err(format!( + "Unable to translate the operations in the circuit: \ + {:?} to the backend's (or manually specified) target \ + basis: {:?}. This likely means the target basis is not universal \ + or there are additional equivalence rules needed in the EquivalenceLibrary being \ + used. For more details on this error see: \ + https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes. \ + BasisTranslator#translation-errors", + source_basis.iter().map(|x| x.0.as_str()).collect_vec(), + &target_basis + ))); + }; + + let instr_map: InstMap = compose_transforms(py, &basis_transforms, &source_basis, &dag)?; + let extra_inst_map: ExtraInstructionMap = qarg_local_basis_transforms + .iter() + .map(|(qarg, transform)| -> PyResult<_> { + Ok(( + qarg, + compose_transforms(py, transform, &qargs_local_source_basis[qarg], &dag)?, + )) + }) + .collect::>()?; + + let (out_dag, _) = + self.apply_translation(py, &dag, &target_basis, &instr_map, &extra_inst_map)?; + Ok(out_dag) + } + + fn __getstate__(slf: PyRef) -> PyResult> { + let state = PyDict::new_bound(slf.py()); + state.set_item( + intern!(slf.py(), "equiv_lib"), + slf.equiv_lib.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "target_basis"), + slf.target_basis.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "target"), + slf.target.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "non_global_operations"), + slf.non_global_operations.clone().into_py(slf.py()), + )?; + state.set_item( + intern!(slf.py(), "qargs_with_non_global_operation"), + slf.qargs_with_non_global_operation + .clone() + .into_py(slf.py()), + )?; + state.set_item(intern!(slf.py(), "min_qubits"), slf.min_qubits)?; + Ok(state) + } + + fn __setstate__(mut slf: PyRefMut, state: Bound) -> PyResult<()> { + slf.equiv_lib = state + .get_item(intern!(slf.py(), "equiv_lib"))? + .unwrap() + .extract()?; + slf.target_basis = state + .get_item(intern!(slf.py(), "target_basis"))? + .unwrap() + .extract()?; + slf.target = state + .get_item(intern!(slf.py(), "target"))? + .unwrap() + .extract()?; + slf.non_global_operations = state + .get_item(intern!(slf.py(), "non_global_operations"))? + .unwrap() + .extract()?; + slf.qargs_with_non_global_operation = state + .get_item(intern!(slf.py(), "qargs_with_non_global_operation"))? + .unwrap() + .extract()?; + slf.min_qubits = state + .get_item(intern!(slf.py(), "min_qubits"))? + .unwrap() + .extract()?; + Ok(()) + } + + fn __getnewargs__(slf: PyRef) -> PyResult { + let py = slf.py(); + Ok(( + slf.equiv_lib.clone(), + slf.min_qubits, + slf.target_basis.clone(), + slf.target.clone(), + ) + .into_py(py)) + } +} + +impl BasisTranslator { + /// Method that extracts all non-calibrated gate instances identifiers from a DAGCircuit. + fn extract_basis(&self, py: Python, circuit: &DAGCircuit) -> PyResult> { + let mut basis = HashSet::default(); + // Recurse for DAGCircuit + fn recurse_dag( + py: Python, + circuit: &DAGCircuit, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + for node in circuit.op_nodes(true) { + let Some(NodeType::Operation(operation)) = circuit.dag().node_weight(node) else { + unreachable!("Circuit op_nodes() returned a non-op node.") + }; + if !circuit.has_calibration_for_index(py, node)? + && circuit.get_qargs(operation.qubits).len() >= min_qubits + { + basis.insert((operation.op.name().to_string(), operation.op.num_qubits())); + } + if operation.op.control_flow() { + let OperationRef::Instruction(inst) = operation.op.view() else { + unreachable!("Control flow operation is not an instance of PyInstruction.") + }; + let inst_bound = inst.instruction.bind(py); + for block in inst_bound.getattr("blocks")?.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + // Recurse for QuantumCircuit + fn recurse_circuit( + py: Python, + circuit: Bound, + basis: &mut HashSet, + min_qubits: usize, + ) -> PyResult<()> { + let circuit_data: PyRef = circuit + .getattr(intern!(py, "_data"))? + .downcast_into()? + .borrow(); + for (index, inst) in circuit_data.iter().enumerate() { + let instruction_object = circuit.get_item(index)?; + let has_calibration = circuit + .call_method1(intern!(py, "has_calibration_for"), (&instruction_object,))?; + if !has_calibration.is_truthy()? + && circuit_data.get_qargs(inst.qubits).len() >= min_qubits + { + basis.insert((inst.op.name().to_string(), inst.op.num_qubits())); + } + if inst.op.control_flow() { + let operation_ob = instruction_object.getattr(intern!(py, "operation"))?; + let blocks = operation_ob.getattr("blocks")?; + for block in blocks.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; + } + } + } + Ok(()) + } + + recurse_dag(py, circuit, &mut basis, self.min_qubits)?; + Ok(basis) + } + + /// Method that extracts a mapping of all the qargs in the local_source basis + /// obtained from the [Target], to all non-calibrated gate instances identifiers from a DAGCircuit. + /// When dealing with `ControlFlowOp` instances the function will perform a recursion call + /// to a variant design to handle instances of `QuantumCircuit`. + fn extract_basis_target( + &self, + py: Python, + dag: &DAGCircuit, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + ) -> PyResult<()> { + for node in dag.op_nodes(true) { + let Some(NodeType::Operation(node_obj)) = dag.dag().node_weight(node) else { + unreachable!("This was supposed to be an op_node.") + }; + let qargs = dag.get_qargs(node_obj.qubits); + if dag.has_calibration_for_index(py, node)? || qargs.len() < self.min_qubits { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if self + .qargs_with_non_global_operation + .contains_key(&Some(physical_qargs)) + || self + .qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!( + "Control flow op is not a control flow op. But control_flow is `true`" + ) + }; + let bound_inst = op.instruction.bind(py); + // Use python side extraction instead of the Rust method `op.blocks` due to + // required usage of a python-space method `QuantumCircuit.has_calibration_for`. + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + self.extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + )?; + } + } + } + Ok(()) + } + + /// Variant of extract_basis_target that takes an instance of QuantumCircuit. + /// This needs to use a Python instance of `QuantumCircuit` due to it needing + /// to access `has_calibration_for()` which is unavailable through rust. However, + /// this API will be removed with the deprecation of `Pulse`. + fn extract_basis_target_circ( + &self, + circuit: &Bound, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + ) -> PyResult<()> { + let py = circuit.py(); + let circ_data_bound = circuit.getattr("_data")?.downcast_into::()?; + let circ_data = circ_data_bound.borrow(); + for (index, node_obj) in circ_data.iter().enumerate() { + let qargs = circ_data.get_qargs(node_obj.qubits); + if circuit + .call_method1("has_calibration_for", (circuit.get_item(index)?,))? + .is_truthy()? + || qargs.len() < self.min_qubits + { + continue; + } + // Treat the instruction as on an incomplete basis if the qargs are in the + // qargs_with_non_global_operation dictionary or if any of the qubits in qargs + // are a superset for a non-local operation. For example, if the qargs + // are (0, 1) and that's a global (ie no non-local operations on (0, 1) + // operation but there is a non-local operation on (1,) we need to + // do an extra non-local search for this op to ensure we include any + // single qubit operation for (1,) as valid. This pattern also holds + // true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, + // and 1q operations in the same manner) + let physical_qargs: SmallVec<[PhysicalQubit; 2]> = + qargs.iter().map(|x| PhysicalQubit(x.0)).collect(); + let physical_qargs_as_set: HashSet = + HashSet::from_iter(physical_qargs.iter().copied()); + if self + .qargs_with_non_global_operation + .contains_key(&Some(physical_qargs)) + || self + .qargs_with_non_global_operation + .keys() + .flatten() + .any(|incomplete_qargs| { + let incomplete_qargs = HashSet::from_iter(incomplete_qargs.iter().copied()); + physical_qargs_as_set.is_superset(&incomplete_qargs) + }) + { + qargs_local_source_basis + .entry(Some(physical_qargs_as_set.into_iter().collect())) + .and_modify(|set| { + set.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + }) + .or_insert(HashSet::from_iter([( + node_obj.op.name().to_string(), + node_obj.op.num_qubits(), + )])); + } else { + source_basis.insert((node_obj.op.name().to_string(), node_obj.op.num_qubits())); + } + if node_obj.op.control_flow() { + let OperationRef::Instruction(op) = node_obj.op.view() else { + unreachable!( + "Control flow op is not a control flow op. But control_flow is `true`" + ) + }; + let bound_inst = op.instruction.bind(py); + let blocks = bound_inst.getattr("blocks")?.iter()?; + for block in blocks { + self.extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + )?; + } + } + } + Ok(()) + } + + fn apply_translation( + &mut self, + py: Python, + dag: &DAGCircuit, + target_basis: &HashSet, + instr_map: &InstMap, + extra_inst_map: &ExtraInstructionMap, + ) -> PyResult<(DAGCircuit, bool)> { + let mut is_updated = false; + let mut out_dag = dag.copy_empty_like(py, "alike")?; + for node in dag.topological_op_nodes()? { + let Some(NodeType::Operation(node_obj)) = dag.dag().node_weight(node).cloned() else { + unreachable!("Node {:?} was in the output of topological_op_nodes, but doesn't seem to be an op_node", node) + }; + let node_qarg = dag.get_qargs(node_obj.qubits); + let node_carg = dag.get_cargs(node_obj.clbits); + let qubit_set: HashSet = HashSet::from_iter(node_qarg.iter().copied()); + let mut new_op: Option = None; + if target_basis.contains(node_obj.op.name()) || node_qarg.len() < self.min_qubits { + if node_obj.op.control_flow() { + let OperationRef::Instruction(control_op) = node_obj.op.view() else { + unreachable!("This instruction {} says it is of control flow type, but is not an Instruction instance", node_obj.op.name()) + }; + let mut flow_blocks = vec![]; + let bound_obj = control_op.instruction.bind(py); + let blocks = bound_obj.getattr("blocks")?; + for block in blocks.iter()? { + let block = block?; + let dag_block: DAGCircuit = + circuit_to_dag(py, block.extract()?, true, None, None)?; + let updated_dag: DAGCircuit; + (updated_dag, is_updated) = self.apply_translation( + py, + &dag_block, + target_basis, + instr_map, + extra_inst_map, + )?; + let flow_circ_block = if is_updated { + DAG_TO_CIRCUIT + .get_bound(py) + .call1((updated_dag,))? + .extract()? + } else { + block + }; + flow_blocks.push(flow_circ_block); + } + let replaced_blocks = + bound_obj.call_method1("replace_blocks", (flow_blocks,))?; + new_op = Some(replaced_blocks.extract()?); + } + if let Some(new_op) = new_op { + out_dag.apply_operation_back( + py, + new_op.operation, + node_qarg, + node_carg, + if new_op.params.is_empty() { + None + } else { + Some(new_op.params) + }, + new_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + } else { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + continue; + } + let node_qarg_as_physical: Option = + Some(node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect()); + if self + .qargs_with_non_global_operation + .contains_key(&node_qarg_as_physical) + && self.qargs_with_non_global_operation[&node_qarg_as_physical] + .contains(node_obj.op.name()) + { + // out_dag.push_back(py, node_obj)?; + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + + if dag.has_calibration_for_index(py, node)? { + out_dag.apply_operation_back( + py, + node_obj.op.clone(), + node_qarg, + node_carg, + if node_obj.params_view().is_empty() { + None + } else { + Some( + node_obj + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(), + ) + }, + node_obj.extra_attrs, + #[cfg(feature = "cache_pygates")] + None, + )?; + continue; + } + let unique_qargs: Option = if qubit_set.is_empty() { + None + } else { + Some(qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect()) + }; + if extra_inst_map.contains_key(&unique_qargs) { + self.replace_node(py, &mut out_dag, node_obj, &extra_inst_map[&unique_qargs])?; + } else if instr_map + .contains_key(&(node_obj.op.name().to_string(), node_obj.op.num_qubits())) + { + self.replace_node(py, &mut out_dag, node_obj, instr_map)?; + } else { + return Err(TranspilerError::new_err(format!( + "BasisTranslator did not map {}", + node_obj.op.name() + ))); + } + is_updated = true; + } + + Ok((out_dag, is_updated)) + } + + fn replace_node( + &mut self, + py: Python, + dag: &mut DAGCircuit, + node: PackedInstruction, + instr_map: &HashMap, DAGCircuit)>, + ) -> PyResult<()> { + let (target_params, target_dag) = + &instr_map[&(node.op.name().to_string(), node.op.num_qubits())]; + if node.params_view().len() != target_params.len() { + return Err(TranspilerError::new_err(format!( + "Translation num_params not equal to op num_params. \ + Op: {:?} {} Translation: {:?}\n{:?}", + node.params_view(), + node.op.name(), + &target_params, + &target_dag + ))); + } + if node.params_view().is_empty() { + for inner_index in target_dag.topological_op_nodes()? { + let NodeType::Operation(inner_node) = &target_dag.dag()[inner_index] else { + unreachable!("Node returned by topological_op_nodes was not an Operation node.") + }; + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + if node.condition().is_some() { + match new_op.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr(py, "condition", node.condition())? + } + OperationRef::Instruction(inst) => { + inst.instruction + .setattr(py, "condition", node.condition())? + } + OperationRef::Operation(oper) => { + oper.operation.setattr(py, "condition", node.condition())? + } + _ => (), + } + } + let new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + let new_extra_props = node.extra_attrs.clone(); + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + new_extra_props, + #[cfg(feature = "cache_pygates")] + None, + )?; + } + dag.add_global_phase(py, target_dag.global_phase())?; + } else { + let parameter_map = target_params + .iter() + .zip(node.params_view()) + .into_py_dict_bound(py); + for inner_index in target_dag.topological_op_nodes()? { + let NodeType::Operation(inner_node) = &target_dag.dag()[inner_index] else { + unreachable!("Node returned by topological_op_nodes was not an Operation node.") + }; + let old_qargs = dag.get_qargs(node.qubits); + let old_cargs = dag.get_cargs(node.clbits); + let new_qubits: Vec = target_dag + .get_qargs(inner_node.qubits) + .iter() + .map(|qubit| old_qargs[qubit.0 as usize]) + .collect(); + let new_clbits: Vec = target_dag + .get_cargs(inner_node.clbits) + .iter() + .map(|clbit| old_cargs[clbit.0 as usize]) + .collect(); + let new_op = if inner_node.op.try_standard_gate().is_none() { + inner_node.op.py_copy(py)? + } else { + inner_node.op.clone() + }; + let mut new_params: SmallVec<[Param; 3]> = inner_node + .params_view() + .iter() + .map(|param| param.clone_ref(py)) + .collect(); + if inner_node + .params_view() + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))) + { + new_params = SmallVec::new(); + for param in inner_node.params_view() { + if let Param::ParameterExpression(param_obj) = param { + let bound_param = param_obj.bind(py); + let exp_params = param.iter_parameters(py)?; + let bind_dict = PyDict::new_bound(py); + for key in exp_params { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_value: Bound; + let comparison = bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }); + if comparison { + new_value = bound_param.clone(); + for items in bind_dict.items() { + new_value = new_value.call_method1( + intern!(py, "assign"), + items.downcast::()?, + )?; + } + } else { + new_value = + bound_param.call_method1(intern!(py, "bind"), (&bind_dict,))?; + } + let eval = new_value.getattr(intern!(py, "parameters"))?; + if eval.is_empty()? { + new_value = new_value.call_method0(intern!(py, "numeric"))?; + } + new_params.push(new_value.extract()?); + } else { + new_params.push(param.clone_ref(py)); + } + } + if new_op.try_standard_gate().is_none() { + match new_op.view() { + OperationRef::Instruction(inst) => inst + .instruction + .bind(py) + .setattr("params", new_params.clone())?, + OperationRef::Gate(gate) => { + gate.gate.bind(py).setattr("params", new_params.clone())? + } + OperationRef::Operation(oper) => oper + .operation + .bind(py) + .setattr("params", new_params.clone())?, + _ => (), + } + } + } + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None + } else { + Some(new_params) + }, + inner_node.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + + if let Param::ParameterExpression(old_phase) = target_dag.global_phase() { + let bound_old_phase = old_phase.bind(py); + let bind_dict = PyDict::new_bound(py); + for key in target_dag.global_phase().iter_parameters(py)? { + let key = key?; + bind_dict.set_item(&key, parameter_map.get_item(&key)?)?; + } + let mut new_phase: Bound; + if bind_dict.values().iter().any(|param| { + param + .is_instance(PARAMETER_EXPRESSION.get_bound(py)) + .is_ok_and(|x| x) + }) { + new_phase = bound_old_phase.clone(); + for key_val in bind_dict.items() { + new_phase = + new_phase.call_method1(intern!(py, "assign"), key_val.downcast()?)?; + } + } else { + new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?; + } + if !new_phase.getattr(intern!(py, "parameters"))?.is_truthy()? { + new_phase = new_phase.call_method0(intern!(py, "numeric"))?; + if new_phase.is_instance(&PyComplex::type_object_bound(py))? { + return Err(TranspilerError::new_err(format!( + "Global phase must be real, but got {}", + new_phase.repr()? + ))); + } + } + let new_phase: Param = new_phase.extract()?; + dag.add_global_phase(py, &new_phase)?; + } + } + + Ok(()) + } +} + #[pymodule] pub fn basis_translator(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(basis_search::py_basis_search))?; - m.add_wrapped(wrap_pyfunction!(compose_transforms::py_compose_transforms))?; + m.add_class::()?; Ok(()) } diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 65fc8e80d750..dfa573eefd08 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -43,7 +43,7 @@ use instruction_properties::InstructionProperties; use self::exceptions::TranspilerError; -mod exceptions { +pub(crate) mod exceptions { use pyo3::import_exception_bound; import_exception_bound! {qiskit.exceptions, QiskitError} import_exception_bound! {qiskit.transpiler.exceptions, TranspilerError} diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a1d3e7f0d39c..442f87d0d984 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -13,23 +13,10 @@ """Translates gates to a target basis using a given equivalence library.""" -import time import logging -from functools import singledispatchmethod -from collections import defaultdict - -from qiskit.circuit import ( - ControlFlowOp, - QuantumCircuit, - ParameterExpression, -) -from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.basis.basis_translator import basis_search, compose_transforms +from qiskit._accelerate.basis.basis_translator import CoreBasisTranslator logger = logging.getLogger(__name__) @@ -111,18 +98,12 @@ def __init__(self, equivalence_library, target_basis, target=None, min_qubits=0) """ super().__init__() - self._equiv_lib = equivalence_library - self._target_basis = target_basis - self._target = target - self._non_global_operations = None - self._qargs_with_non_global_operation = {} - self._min_qubits = min_qubits - if target is not None: - self._non_global_operations = self._target.get_non_global_operation_names() - self._qargs_with_non_global_operation = defaultdict(set) - for gate in self._non_global_operations: - for qarg in self._target[gate]: - self._qargs_with_non_global_operation[qarg].add(gate) + self._core = CoreBasisTranslator( + equivalence_library, + min_qubits, + None if target_basis is None else set(target_basis), + target, + ) def run(self, dag): """Translate an input DAGCircuit to the target basis. @@ -136,309 +117,5 @@ def run(self, dag): Returns: DAGCircuit: translated circuit. """ - if self._target_basis is None and self._target is None: - return dag - - qarg_indices = {qubit: index for index, qubit in enumerate(dag.qubits)} - - # Names of instructions assumed to supported by any backend. - if self._target is None: - basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] - target_basis = set(self._target_basis) - source_basis = set(self._extract_basis(dag)) - qargs_local_source_basis = {} - else: - basic_instrs = ["barrier", "snapshot", "store"] - target_basis = self._target.keys() - set(self._non_global_operations) - source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) - - target_basis = set(target_basis).union(basic_instrs) - # If the source basis is a subset of the target basis and we have no circuit - # instructions on qargs that have non-global operations there is nothing to - # translate and we can exit early. - source_basis_names = {x[0] for x in source_basis} - if source_basis_names.issubset(target_basis) and not qargs_local_source_basis: - return dag - - logger.info( - "Begin BasisTranslator from source basis %s to target basis %s.", - source_basis, - target_basis, - ) - - # Search for a path from source to target basis. - search_start_time = time.time() - basis_transforms = basis_search(self._equiv_lib, source_basis, target_basis) - - qarg_local_basis_transforms = {} - for qarg, local_source_basis in qargs_local_source_basis.items(): - expanded_target = set(target_basis) - # For any multiqubit operation that contains a subset of qubits that - # has a non-local operation, include that non-local operation in the - # search. This matches with the check we did above to include those - # subset non-local operations in the check here. - if len(qarg) > 1: - for non_local_qarg, local_basis in self._qargs_with_non_global_operation.items(): - if qarg.issuperset(non_local_qarg): - expanded_target |= local_basis - else: - expanded_target |= self._qargs_with_non_global_operation[tuple(qarg)] - - logger.info( - "Performing BasisTranslator search from source basis %s to target " - "basis %s on qarg %s.", - local_source_basis, - expanded_target, - qarg, - ) - local_basis_transforms = basis_search( - self._equiv_lib, local_source_basis, expanded_target - ) - - if local_basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in local_source_basis]} to the backend's (or manually " - f"specified) target basis: {list(expanded_target)}. This likely means the " - "target basis is not universal or there are additional equivalence rules " - "needed in the EquivalenceLibrary being used. For more details on this " - "error see: " - "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - qarg_local_basis_transforms[qarg] = local_basis_transforms - - search_end_time = time.time() - logger.info( - "Basis translation path search completed in %.3fs.", search_end_time - search_start_time - ) - - if basis_transforms is None: - raise TranspilerError( - "Unable to translate the operations in the circuit: " - f"{[x[0] for x in source_basis]} to the backend's (or manually specified) target " - f"basis: {list(target_basis)}. This likely means the target basis is not universal " - "or there are additional equivalence rules needed in the EquivalenceLibrary being " - "used. For more details on this error see: " - "https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.passes." - "BasisTranslator#translation-errors" - ) - - # Compose found path into a set of instruction substitution rules. - - compose_start_time = time.time() - instr_map = compose_transforms(basis_transforms, source_basis, dag) - extra_instr_map = { - qarg: compose_transforms(transforms, qargs_local_source_basis[qarg], dag) - for qarg, transforms in qarg_local_basis_transforms.items() - } - - compose_end_time = time.time() - logger.info( - "Basis translation paths composed in %.3fs.", compose_end_time - compose_start_time - ) - - # Replace source instructions with target translations. - - replace_start_time = time.time() - - def apply_translation(dag, wire_map): - is_updated = False - out_dag = dag.copy_empty_like() - for node in dag.topological_op_nodes(): - node_qargs = tuple(wire_map[bit] for bit in node.qargs) - qubit_set = frozenset(node_qargs) - if node.name in target_basis or len(node.qargs) < self._min_qubits: - if node.name in CONTROL_FLOW_OP_NAMES: - flow_blocks = [] - for block in node.op.blocks: - dag_block = circuit_to_dag(block) - updated_dag, is_updated = apply_translation( - dag_block, - { - inner: wire_map[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - ) - if is_updated: - flow_circ_block = dag_to_circuit(updated_dag) - else: - flow_circ_block = block - flow_blocks.append(flow_circ_block) - node.op = node.op.replace_blocks(flow_blocks) - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if ( - node_qargs in self._qargs_with_non_global_operation - and node.name in self._qargs_with_non_global_operation[node_qargs] - ): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - - if dag.has_calibration_for(node): - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - continue - if qubit_set in extra_instr_map: - self._replace_node(out_dag, node, extra_instr_map[qubit_set]) - elif (node.name, node.num_qubits) in instr_map: - self._replace_node(out_dag, node, instr_map) - else: - raise TranspilerError(f"BasisTranslator did not map {node.name}.") - is_updated = True - return out_dag, is_updated - - out_dag, _ = apply_translation(dag, qarg_indices) - replace_end_time = time.time() - logger.info( - "Basis translation instructions replaced in %.3fs.", - replace_end_time - replace_start_time, - ) - - return out_dag - - def _replace_node(self, dag, node, instr_map): - target_params, target_dag = instr_map[node.name, node.num_qubits] - if len(node.params) != len(target_params): - raise TranspilerError( - "Translation num_params not equal to op num_params." - f"Op: {node.params} {node.name} Translation: {target_params}\n{target_dag}" - ) - if node.params: - parameter_map = dict(zip(target_params, node.params)) - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction(inner_node._to_circuit_instruction()) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - - if not new_node.is_standard_gate(): - new_node.op = new_node.op.copy() - if any(isinstance(x, ParameterExpression) for x in inner_node.params): - new_params = [] - for param in new_node.params: - if not isinstance(param, ParameterExpression): - new_params.append(param) - else: - bind_dict = {x: parameter_map[x] for x in param.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_value = param - for x in bind_dict.items(): - new_value = new_value.assign(*x) - else: - new_value = param.bind(bind_dict) - if not new_value.parameters: - new_value = new_value.numeric() - new_params.append(new_value) - new_node.params = new_params - if not new_node.is_standard_gate(): - new_node.op.params = new_params - dag._apply_op_node_back(new_node) - - if isinstance(target_dag.global_phase, ParameterExpression): - old_phase = target_dag.global_phase - bind_dict = {x: parameter_map[x] for x in old_phase.parameters} - if any(isinstance(x, ParameterExpression) for x in bind_dict.values()): - new_phase = old_phase - for x in bind_dict.items(): - new_phase = new_phase.assign(*x) - else: - new_phase = old_phase.bind(bind_dict) - if not new_phase.parameters: - new_phase = new_phase.numeric() - if isinstance(new_phase, complex): - raise TranspilerError(f"Global phase must be real, but got '{new_phase}'") - dag.global_phase += new_phase - - else: - for inner_node in target_dag.topological_op_nodes(): - new_node = DAGOpNode.from_instruction( - inner_node._to_circuit_instruction(), - ) - new_node.qargs = tuple( - node.qargs[target_dag.find_bit(x).index] for x in inner_node.qargs - ) - new_node.cargs = tuple( - node.cargs[target_dag.find_bit(x).index] for x in inner_node.cargs - ) - if not new_node.is_standard_gate: - new_node.op = new_node.op.copy() - # dag_op may be the same instance as other ops in the dag, - # so if there is a condition, need to copy - if getattr(node.op, "condition", None): - new_node_op = new_node.op.to_mutable() - new_node_op.condition = node.op.condition - new_node.op = new_node_op - dag._apply_op_node_back(new_node) - if target_dag.global_phase: - dag.global_phase += target_dag.global_phase - - @singledispatchmethod - def _extract_basis(self, circuit): - return circuit - - @_extract_basis.register - def _(self, dag: DAGCircuit): - for node in dag.op_nodes(): - if not dag.has_calibration_for(node) and len(node.qargs) >= self._min_qubits: - yield (node.name, node.num_qubits) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - yield from self._extract_basis(block) - - @_extract_basis.register - def _(self, circ: QuantumCircuit): - for instruction in circ.data: - operation = instruction.operation - if ( - not circ.has_calibration_for(instruction) - and len(instruction.qubits) >= self._min_qubits - ): - yield (operation.name, operation.num_qubits) - if isinstance(operation, ControlFlowOp): - for block in operation.blocks: - yield from self._extract_basis(block) - def _extract_basis_target( - self, dag, qarg_indices, source_basis=None, qargs_local_source_basis=None - ): - if source_basis is None: - source_basis = set() - if qargs_local_source_basis is None: - qargs_local_source_basis = defaultdict(set) - for node in dag.op_nodes(): - qargs = tuple(qarg_indices[bit] for bit in node.qargs) - if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: - continue - # Treat the instruction as on an incomplete basis if the qargs are in the - # qargs_with_non_global_operation dictionary or if any of the qubits in qargs - # are a superset for a non-local operation. For example, if the qargs - # are (0, 1) and that's a global (ie no non-local operations on (0, 1) - # operation but there is a non-local operation on (1,) we need to - # do an extra non-local search for this op to ensure we include any - # single qubit operation for (1,) as valid. This pattern also holds - # true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, - # and 1q operations in the same manner) - if qargs in self._qargs_with_non_global_operation or any( - frozenset(qargs).issuperset(incomplete_qargs) - for incomplete_qargs in self._qargs_with_non_global_operation - ): - qargs_local_source_basis[frozenset(qargs)].add((node.name, node.num_qubits)) - else: - source_basis.add((node.name, node.num_qubits)) - if node.name in CONTROL_FLOW_OP_NAMES: - for block in node.op.blocks: - block_dag = circuit_to_dag(block) - source_basis, qargs_local_source_basis = self._extract_basis_target( - block_dag, - { - inner: qarg_indices[outer] - for inner, outer in zip(block.qubits, node.qargs) - }, - source_basis=source_basis, - qargs_local_source_basis=qargs_local_source_basis, - ) - return source_basis, qargs_local_source_basis + return self._core.run(dag)