From 3c00fdd4dd7b021a12d523ebd07f769705b975ab Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:21:06 -0400 Subject: [PATCH 1/8] Initial: Move the rest of the `BasisTranslator` to Rust. Fixes Port `BasisTranslator` to Rust #12246 This is the final act of the efforts to move the `BasisTranslator` transpiler pass into Rust. With many of the parts of this pass already living in Rust, the following commits attempt to bring in the final changes to allow complete operation of this pass in the Rust space (with of course some interaction with Python.) Methodology: The way this works is by keeping the original `BasisTranslator` python class, and have it store the rust-space counterpart (now called `CoreBasisTranslator`) which will perform all of the operations leveraging the existent Rust API's available for the `Target` (#12292), `EquivalenceLibrary`(#12585) and the `BasisTranslator` methods `basis_search` (#12811) and `compose_transforms`(#13137). All of the inner methods will have private visibility and will not be accessible to `Python` as they're intended to be internal by design. By removing the extra layers of conversion we should be seeing a considerable speed-up, alongside all of the other incremental improvements we have made. Changes: - Add the pyo3 class/struct `BasisTranslator` that will contain allof the main data used by the transpiler pass to perform its operation. - Convert the `target_basis` into a set manually from python before sending it into the Rust space. - Remove the exposure of `basis_search` and `compose_transforms` to python. - Change `basis_search` so that it accepts references to `HashSet` instances instead of accepting a `HashSet<&str>` instance. - Change inner method's visibility for `basis_search` and `compose_transform` modules in rust. - Expose the exception imports from `Target` to the `accelerate` crate. - Expose `DAGCircuit::copy_empty_like` to the rest of the crates. - Remove all of the unused imports in the Python-side `BasisTranslator`. Blockers: - [ ] #12811 --- .../basis/basis_translator/basis_search.rs | 23 - .../basis_translator/compose_transforms.rs | 10 - .../src/basis/basis_translator/mod.rs | 906 +++++++++++++++++- .../accelerate/src/target_transpiler/mod.rs | 2 +- .../passes/basis/basis_translator.py | 339 +------ 5 files changed, 912 insertions(+), 368 deletions(-) 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) From 3f00a732940da56bc55f0c9c10867b70c0bf63a2 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 8 Oct 2024 11:47:59 -0400 Subject: [PATCH 2/8] Fix: Redundancies with serialization methods - Remove extra copies of `target`, `target_basis`, `equiv_lib`, and `min_qubits`. - Remove unnecessary mutability in `apply_transforms` and `replace_node`. --- .../src/basis/basis_translator/mod.rs | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 727d041a1947..b29736a4390f 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -233,18 +233,6 @@ impl BasisTranslator { 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()), @@ -255,23 +243,10 @@ impl BasisTranslator { .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() @@ -280,10 +255,6 @@ impl BasisTranslator { .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(()) } @@ -525,7 +496,7 @@ impl BasisTranslator { } fn apply_translation( - &mut self, + &self, py: Python, dag: &DAGCircuit, target_basis: &HashSet, @@ -694,7 +665,7 @@ impl BasisTranslator { } fn replace_node( - &mut self, + &self, py: Python, dag: &mut DAGCircuit, node: PackedInstruction, From 81bdec45075b9fee89a7c9e1f2e9c61ff31e8718 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:36:05 -0400 Subject: [PATCH 3/8] Refactor: Remove `BasisTranslator` struct, use pymethod. - Using this method avoids the creation of a datastructure in rust and the overhead of deserializing rust structures which can be overly slow due to multiple cloning. With this update, since the `BasisTranslator` never mutates, it is better to not store anything in Rust. --- .../src/basis/basis_translator/mod.rs | 1393 ++++++++--------- .../passes/basis/basis_translator.py | 32 +- 2 files changed, 682 insertions(+), 743 deletions(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index b29736a4390f..17dfa3c8df21 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -45,580 +45,452 @@ 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, - } +#[pyfunction(name = "base_run")] +fn run( + py: Python<'_>, + dag: DAGCircuit, + equiv_lib: &mut EquivalenceLibrary, + qargs_with_non_global_operation: HashMap, HashSet>, + min_qubits: usize, + target_basis: Option>, + target: Option<&Target>, + non_global_operations: Option>, +) -> PyResult { + if target_basis.is_none() && target.is_none() { + return Ok(dag); } - 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)?; + let basic_instrs: HashSet; + let mut source_basis: HashSet = HashSet::default(); + let mut new_target_basis: HashSet; + let mut qargs_local_source_basis: HashMap, HashSet> = + HashMap::default(); + if let Some(target) = 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) = non_global_operations.as_ref() + { + operations.iter().map(|x| x.as_str()).collect() } 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) + HashSet::default() + }; + let target_keys = target.keys().collect::>(); + new_target_basis = target_keys + .difference(&non_global_str) .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(); - } + extract_basis_target( + py, + &dag, + &mut source_basis, + &mut qargs_local_source_basis, + min_qubits, + &qargs_with_non_global_operation, + )?; + } else { + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] + .into_iter() + .map(|x| x.to_string()) + .collect(); + source_basis = extract_basis(py, &dag, min_qubits)?; + new_target_basis = target_basis.clone().unwrap(); + } + new_target_basis = new_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(&new_target_basis) && qargs_local_source_basis.is_empty() { + return Ok(dag); + } + let basis_transforms = basis_search(equiv_lib, &source_basis, &new_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 = new_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 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 - ))); } + } else { + expanded_target = expanded_target + .union(&qargs_with_non_global_operation[qarg]) + .cloned() + .collect(); } - - let Some(basis_transforms) = basis_transforms else { + let local_basis_transforms = basis_search(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. \ + 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 + local_source_basis + .iter() + .map(|x| x.0.as_str()) + .collect_vec(), + &expanded_target ))); - }; - - 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(), "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()), - )?; - Ok(state) - } + 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(), + &new_target_basis + ))); + }; - fn __setstate__(mut slf: PyRefMut, state: Bound) -> PyResult<()> { - 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()?; - Ok(()) - } + 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::>()?; - 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)) - } + let (out_dag, _) = apply_translation( + py, + &dag, + &new_target_basis, + &instr_map, + &extra_inst_map, + min_qubits, + &qargs_with_non_global_operation, + )?; + Ok(out_dag) } -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, +/// Method that extracts all non-calibrated gate instances identifiers from a DAGCircuit. +fn extract_basis( + py: Python, + circuit: &DAGCircuit, + min_qubits: usize, +) -> PyResult> { + let mut basis = HashSet::default(); + // Recurse for DAGCircuit + fn recurse_dag( py: Python, - dag: &DAGCircuit, - source_basis: &mut HashSet, - qargs_local_source_basis: &mut HashMap, HashSet>, + circuit: &DAGCircuit, + basis: &mut HashSet, + min_qubits: usize, ) -> 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.") + 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.") }; - 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) - }) + if !circuit.has_calibration_for_index(py, node)? + && circuit.get_qargs(operation.qubits).len() >= min_qubits { - 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())); + basis.insert((operation.op.name().to_string(), operation.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`" - ) + if operation.op.control_flow() { + let OperationRef::Instruction(inst) = operation.op.view() else { + unreachable!("Control flow operation is not an instance of PyInstruction.") }; - 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, - )?; + let inst_bound = inst.instruction.bind(py); + for block in inst_bound.getattr("blocks")?.iter()? { + recurse_circuit(py, block?, basis, min_qubits)?; } } } 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>, + // Recurse for QuantumCircuit + fn recurse_circuit( + py: Python, + circuit: Bound, + basis: &mut HashSet, + min_qubits: usize, ) -> 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 + 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 { - continue; + basis.insert((inst.op.name().to_string(), inst.op.num_qubits())); } - // 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, - )?; + 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(()) } - fn apply_translation( - &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) + recurse_dag(py, circuit, &mut basis, 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( + py: Python, + dag: &DAGCircuit, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + min_qubits: usize, + qargs_with_non_global_operation: &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() < 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 qargs_with_non_global_operation.contains_key(&Some(physical_qargs)) + || 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 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( + 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 { + extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + min_qubits, + qargs_with_non_global_operation, + )?; + } + } + } + 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( + circuit: &Bound, + source_basis: &mut HashSet, + qargs_local_source_basis: &mut HashMap, HashSet>, + min_qubits: usize, + qargs_with_non_global_operation: &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() < 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 qargs_with_non_global_operation.contains_key(&Some(physical_qargs)) + || 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 { + extract_basis_target_circ( + &block?, + source_basis, + qargs_local_source_basis, + min_qubits, + qargs_with_non_global_operation, + )?; + } + } + } + Ok(()) +} + +fn apply_translation( + py: Python, + dag: &DAGCircuit, + target_basis: &HashSet, + instr_map: &InstMap, + extra_inst_map: &ExtraInstructionMap, + min_qubits: usize, + qargs_with_non_global_operation: &HashMap, HashSet>, +) -> 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() < 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) = apply_translation( 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, + &dag_block, + target_basis, + instr_map, + extra_inst_map, + min_qubits, + qargs_with_non_global_operation, )?; + 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); } - continue; + let replaced_blocks = bound_obj.call_method1("replace_blocks", (flow_blocks,))?; + new_op = Some(replaced_blocks.extract()?); } - 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)?; + if let Some(new_op) = new_op { out_dag.apply_operation_back( py, - node_obj.op.clone(), + new_op.operation, node_qarg, node_carg, - if node_obj.params_view().is_empty() { + if new_op.params.is_empty() { None } else { - Some( - node_obj - .params_view() - .iter() - .map(|param| param.clone_ref(py)) - .collect(), - ) + Some(new_op.params) }, - node_obj.extra_attrs, + new_op.extra_attrs, #[cfg(feature = "cache_pygates")] None, )?; - continue; - } - - if dag.has_calibration_for_index(py, node)? { + } else { out_dag.apply_operation_back( py, node_obj.op.clone(), @@ -635,260 +507,311 @@ impl BasisTranslator { .collect(), ) }, - node_obj.extra_attrs, + node_obj.extra_attrs.clone(), #[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; + continue; + } + let node_qarg_as_physical: Option = + Some(node_qarg.iter().map(|x| PhysicalQubit(x.0)).collect()); + if qargs_with_non_global_operation.contains_key(&node_qarg_as_physical) + && 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; } - Ok((out_dag, is_updated)) - } - - fn replace_node( - &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() { + 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) { + 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())) + { + replace_node(py, &mut out_dag, node_obj, instr_map)?; + } else { 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 + "BasisTranslator did not map {}", + node_obj.op.name() ))); } - 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())? - } - _ => (), + is_updated = true; + } + + Ok((out_dag, is_updated)) +} + +fn replace_node( + 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 + let new_params: SmallVec<[Param; 3]> = inner_node + .params_view() .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)? + .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 { - 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"))?; + 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::()?, + )?; } - new_params.push(new_value.extract()?); } else { - new_params.push(param.clone_ref(py)); + 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())?, - _ => (), + } + 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()?)?; - } + dag.apply_operation_back( + py, + new_op, + &new_qubits, + &new_clbits, + if new_params.is_empty() { + None } else { - new_phase = bound_old_phase.call_method1(intern!(py, "bind"), (bind_dict,))?; + 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()?)?; } - 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()? - ))); - } + } 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)?; } + let new_phase: Param = new_phase.extract()?; + dag.add_global_phase(py, &new_phase)?; } - - Ok(()) } + + Ok(()) } #[pymodule] pub fn basis_translator(m: &Bound) -> PyResult<()> { - m.add_class::()?; + m.add_wrapped(wrap_pyfunction!(run))?; Ok(()) } diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 442f87d0d984..c6a61b8ad49a 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -15,8 +15,10 @@ import logging +from collections import defaultdict + from qiskit.transpiler.basepasses import TransformationPass -from qiskit._accelerate.basis.basis_translator import CoreBasisTranslator +from qiskit._accelerate.basis.basis_translator import base_run logger = logging.getLogger(__name__) @@ -98,12 +100,18 @@ def __init__(self, equivalence_library, target_basis, target=None, min_qubits=0) """ super().__init__() - self._core = CoreBasisTranslator( - equivalence_library, - min_qubits, - None if target_basis is None else set(target_basis), - target, - ) + 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) def run(self, dag): """Translate an input DAGCircuit to the target basis. @@ -118,4 +126,12 @@ def run(self, dag): DAGCircuit: translated circuit. """ - return self._core.run(dag) + return base_run( + dag, + self._equiv_lib, + self._qargs_with_non_global_operation, + self._min_qubits, + None if self._target_basis is None else set(self._target_basis), + self._target, + None if self._non_global_operations is None else set(self._non_global_operations), + ) From 71f2e1db2a440c5b5b47a112074ed17d490a7859 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:22:42 -0400 Subject: [PATCH 4/8] Lint: Ignore too_many_args flag from clippy on `basis_translator::run()` --- crates/accelerate/src/basis/basis_translator/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 17dfa3c8df21..c16646a5e587 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -48,6 +48,7 @@ use crate::target_transpiler::{Qargs, Target}; type InstMap = HashMap; type ExtraInstructionMap<'a> = HashMap<&'a Option, InstMap>; +#[allow(clippy::too_many_arguments)] #[pyfunction(name = "base_run")] fn run( py: Python<'_>, From 709d7061b8ab362fea81b044fb522379df99bfcd Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 10 Oct 2024 09:57:39 -0400 Subject: [PATCH 5/8] Fix: Remove redundant clone --- crates/accelerate/src/basis/basis_translator/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index c16646a5e587..71df4c849c69 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -99,7 +99,7 @@ fn run( .map(|x| x.to_string()) .collect(); source_basis = extract_basis(py, &dag, min_qubits)?; - new_target_basis = target_basis.clone().unwrap(); + new_target_basis = target_basis.unwrap(); } new_target_basis = new_target_basis .union(&basic_instrs) From c9040ff24bd3f5d2592c55ecb1bb22d8ba226490 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:33:51 -0400 Subject: [PATCH 6/8] Fix: Leverage using `unwrap_operation` when taking op_nodes from the dag. - Add function signature in `base_run`. --- .../src/basis/basis_translator/mod.rs | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 71df4c849c69..f88dd9cbe46e 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -34,7 +34,7 @@ use qiskit_circuit::operations::Param; use qiskit_circuit::packed_instruction::PackedInstruction; use qiskit_circuit::{ circuit_data::CircuitData, - dag_circuit::{DAGCircuit, NodeType}, + dag_circuit::DAGCircuit, operations::{Operation, OperationRef}, }; use qiskit_circuit::{Clbit, Qubit}; @@ -49,7 +49,7 @@ type InstMap = HashMap; type ExtraInstructionMap<'a> = HashMap<&'a Option, InstMap>; #[allow(clippy::too_many_arguments)] -#[pyfunction(name = "base_run")] +#[pyfunction(name = "base_run", signature = (dag, equiv_lib, qargs_with_non_global_operation, min_qubits, target_basis=None, target=None, non_global_operations=None))] fn run( py: Python<'_>, dag: DAGCircuit, @@ -213,9 +213,7 @@ fn extract_basis( 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.") - }; + let operation: &PackedInstruction = circuit.dag()[node].unwrap_operation(); if !circuit.has_calibration_for_index(py, node)? && circuit.get_qargs(operation.qubits).len() >= min_qubits { @@ -282,10 +280,8 @@ fn extract_basis_target( qargs_with_non_global_operation: &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); + let node_obj: &PackedInstruction = dag.dag()[node].unwrap_operation(); + let qargs: &[Qubit] = dag.get_qargs(node_obj.qubits); if dag.has_calibration_for_index(py, node)? || qargs.len() < min_qubits { continue; } @@ -434,9 +430,7 @@ fn apply_translation( 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_obj = dag.dag()[node].unwrap_operation(); 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()); @@ -537,7 +531,7 @@ fn apply_translation( .collect(), ) }, - node_obj.extra_attrs, + node_obj.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] None, )?; @@ -561,7 +555,7 @@ fn apply_translation( .collect(), ) }, - node_obj.extra_attrs, + node_obj.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] None, )?; @@ -573,11 +567,16 @@ fn apply_translation( Some(qubit_set.iter().map(|x| PhysicalQubit(x.0)).collect()) }; if extra_inst_map.contains_key(&unique_qargs) { - replace_node(py, &mut out_dag, node_obj, &extra_inst_map[&unique_qargs])?; + replace_node( + py, + &mut out_dag, + node_obj.clone(), + &extra_inst_map[&unique_qargs], + )?; } else if instr_map .contains_key(&(node_obj.op.name().to_string(), node_obj.op.num_qubits())) { - replace_node(py, &mut out_dag, node_obj, instr_map)?; + replace_node(py, &mut out_dag, node_obj.clone(), instr_map)?; } else { return Err(TranspilerError::new_err(format!( "BasisTranslator did not map {}", @@ -610,9 +609,7 @@ fn replace_node( } 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 inner_node = &target_dag.dag()[inner_index].unwrap_operation(); let old_qargs = dag.get_qargs(node.qubits); let old_cargs = dag.get_cargs(node.clbits); let new_qubits: Vec = target_dag @@ -673,9 +670,7 @@ fn replace_node( .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 inner_node = &target_dag.dag()[inner_index].unwrap_operation(); let old_qargs = dag.get_qargs(node.qubits); let old_cargs = dag.get_cargs(node.clbits); let new_qubits: Vec = target_dag From f8c9c87ba3b8a2946716cf596ac768e57a96b227 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:52:24 -0400 Subject: [PATCH 7/8] Update crates/accelerate/src/basis/basis_translator/mod.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/accelerate/src/basis/basis_translator/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index f88dd9cbe46e..e5ef26b9ff57 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -514,7 +514,6 @@ fn apply_translation( if qargs_with_non_global_operation.contains_key(&node_qarg_as_physical) && 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(), From e369460ac9f462e1007eb285e580e1de9c3be580 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 29 Oct 2024 14:44:03 -0400 Subject: [PATCH 8/8] Adapt to #13164 - Use `QuantumCircuit._has_calibration_for()` when trying to obtain calibrations from a `QuantumCircuit` due to the deprecation of the `Pulse` package. --- crates/accelerate/src/basis/basis_translator/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index e5ef26b9ff57..c900f80beff4 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -245,8 +245,8 @@ fn extract_basis( .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,))?; + 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 { @@ -358,7 +358,7 @@ fn extract_basis_target_circ( 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)?,))? + .call_method1("_has_calibration_for", (circuit.get_item(index)?,))? .is_truthy()? || qargs.len() < min_qubits {