diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 4c570f4a5285..673efe6e7e26 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -9,6 +9,10 @@ license.workspace = true name = "qiskit_accelerate" doctest = false + +[features] +cache_pygates = ["qiskit-circuit/cache_pygates"] + [dependencies] rayon.workspace = true numpy.workspace = true diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index dc4d0b77c4a7..844c02e4d3fc 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -73,14 +73,24 @@ pub fn blocks_to_matrix( for bit in index_map { bit_map.add(py, bit.bind(py), true)?; } + let matrix = compose_2q_matrix(op_list.iter().map(|node| { + let matrix = get_matrix_from_inst(py, &node.instruction)?; + let bit_indices = bit_map + .map_bits(node.instruction.qubits.bind(py).iter())? + .map(|x| x as u8) + .collect::>(); + Ok((matrix, bit_indices)) + }))?; + Ok(matrix.into_pyarray_bound(py).unbind()) +} + +pub fn compose_2q_matrix<'a>( + mut matrices: impl Iterator, SmallVec<[u8; 2]>)>> + 'a, +) -> PyResult> { let identity = aview2(&ONE_QUBIT_IDENTITY); - let first_node = &op_list[0]; - let input_matrix = get_matrix_from_inst(py, &first_node.instruction)?; - let mut matrix: Array2 = match bit_map - .map_bits(first_node.instruction.qubits.bind(py).iter())? - .collect::>() - .as_slice() - { + let (input_matrix, bit_map) = matrices.next().unwrap()?; + + let mut matrix: Array2 = match bit_map.as_slice() { [0] => kron(&identity, &input_matrix), [1] => kron(&input_matrix, &identity), [0, 1] => input_matrix, @@ -88,13 +98,8 @@ pub fn blocks_to_matrix( [] => Array2::eye(4), _ => unreachable!(), }; - for node in op_list.into_iter().skip(1) { - let op_matrix = get_matrix_from_inst(py, &node.instruction)?; - let q_list = bit_map - .map_bits(node.instruction.qubits.bind(py).iter())? - .map(|x| x as u8) - .collect::>(); - + for raw_data in matrices { + let (op_matrix, q_list) = raw_data?; let result = match q_list.as_slice() { [0] => Some(kron(&identity, &op_matrix)), [1] => Some(kron(&op_matrix, &identity)), @@ -107,7 +112,7 @@ pub fn blocks_to_matrix( None => op_matrix.dot(&matrix), }; } - Ok(matrix.into_pyarray_bound(py).unbind()) + Ok(matrix) } /// Switches the order of qubits in a two qubit operation. diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 98333cad39d2..e6ca094186ff 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -579,7 +579,7 @@ pub fn generate_circuit( const EULER_BASIS_SIZE: usize = 12; -static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ +pub static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ &["u3"], &["u3", "u2", "u1"], &["u"], @@ -593,7 +593,7 @@ static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ &["rz", "sx", "x"], &["rz", "sx"], ]; -static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [ +pub static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [ EulerBasis::U3, EulerBasis::U321, EulerBasis::U, diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 9111f932e270..abf35d6ce27a 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -42,6 +42,7 @@ pub mod stochastic_swap; pub mod synthesis; pub mod target_transpiler; pub mod two_qubit_decompose; +pub mod two_qubit_peephole; pub mod uc_gate; pub mod utils; pub mod vf2_layout; diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 65fc8e80d750..93725d9073d5 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -31,6 +31,8 @@ use pyo3::{ types::{PyDict, PyList, PySet, PyTuple}, }; +use ndarray::Array2; +use num_complex::Complex64; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::operations::{Operation, Param}; use qiskit_circuit::packed_instruction::PackedOperation; @@ -108,6 +110,12 @@ pub(crate) struct NormalOperation { op_object: PyObject, } +impl NormalOperation { + pub fn matrix(&self) -> Option> { + self.operation.view().matrix(&self.params) + } +} + impl<'py> FromPyObject<'py> for NormalOperation { fn extract(ob: &'py PyAny) -> PyResult { let operation: OperationFromPython = ob.extract()?; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 76b92acd3faf..bf79bdaac81e 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1216,9 +1216,9 @@ type TwoQubitSequenceVec = Vec<(Option, SmallVec<[f64; 3]>, SmallV #[pyclass(sequence)] pub struct TwoQubitGateSequence { - gates: TwoQubitSequenceVec, + pub gates: TwoQubitSequenceVec, #[pyo3(get)] - global_phase: f64, + pub global_phase: f64, } #[pymethods] @@ -1287,6 +1287,10 @@ pub struct TwoQubitBasisDecomposer { q2r: Array2, } impl TwoQubitBasisDecomposer { + pub fn gate_name(&self) -> &str { + self.gate.as_str() + } + fn decomp1_inner( &self, target: &TwoQubitWeylDecomposition, @@ -1643,11 +1647,11 @@ impl TwoQubitBasisDecomposer { Ok(res) } - fn new_inner( + pub fn new_inner( gate: String, gate_matrix: ArrayView2, basis_fidelity: f64, - euler_basis: &str, + euler_basis: EulerBasis, pulse_optimize: Option, ) -> PyResult { let ipz: ArrayView2 = aview2(&IPZ); @@ -1755,7 +1759,7 @@ impl TwoQubitBasisDecomposer { Ok(TwoQubitBasisDecomposer { gate, basis_fidelity, - euler_basis: EulerBasis::__new__(euler_basis)?, + euler_basis, pulse_optimize, basis_decomposer, super_controlled, @@ -1781,7 +1785,7 @@ impl TwoQubitBasisDecomposer { }) } - fn call_inner( + pub fn call_inner( &self, unitary: ArrayView2, basis_fidelity: Option, @@ -1924,7 +1928,7 @@ impl TwoQubitBasisDecomposer { gate, gate_matrix.as_array(), basis_fidelity, - euler_basis, + EulerBasis::__new__(euler_basis)?, pulse_optimize, ) } @@ -2222,8 +2226,13 @@ fn two_qubit_decompose_up_to_diagonal( let (su4, phase) = u4_to_su4(mat_arr); let mut real_map = real_trace_transform(su4.view()); let mapped_su4 = real_map.dot(&su4.view()); - let decomp = - TwoQubitBasisDecomposer::new_inner("cx".to_string(), aview2(&CX_GATE), 1.0, "U", None)?; + let decomp = TwoQubitBasisDecomposer::new_inner( + "cx".to_string(), + aview2(&CX_GATE), + 1.0, + EulerBasis::__new__("U")?, + None, + )?; let circ_seq = decomp.call_inner(mapped_su4.view(), None, true, None)?; let circ = CircuitData::from_standard_gates( diff --git a/crates/accelerate/src/two_qubit_peephole.rs b/crates/accelerate/src/two_qubit_peephole.rs new file mode 100644 index 000000000000..8f8f2ebf8291 --- /dev/null +++ b/crates/accelerate/src/two_qubit_peephole.rs @@ -0,0 +1,424 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::cmp::Ordering; +use std::sync::Mutex; + +use hashbrown::{HashMap, HashSet}; +use pyo3::prelude::*; +use rayon::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; +use smallvec::{smallvec, SmallVec}; + +use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::packed_instruction::PackedOperation; +use qiskit_circuit::Qubit; + +use crate::convert_2q_block_matrix::compose_2q_matrix; +use crate::euler_one_qubit_decomposer::{ + EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, +}; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::{NormalOperation, Target}; +use crate::two_qubit_decompose::{TwoQubitBasisDecomposer, TwoQubitGateSequence}; + +fn get_decomposers_from_target( + target: &Target, + qubits: &[Qubit], + fidelity: f64, +) -> PyResult> { + let physical_qubits = smallvec![PhysicalQubit(qubits[0].0), PhysicalQubit(qubits[1].0)]; + let gate_names = match target.operation_names_for_qargs(Some(&physical_qubits)) { + Ok(names) => names, + Err(_) => { + let reverse_qubits = physical_qubits.iter().rev().copied().collect(); + target + .operation_names_for_qargs(Some(&reverse_qubits)) + .unwrap() + } + }; + + let available_kak_gate: Vec<(&str, NormalOperation)> = gate_names + .iter() + .filter_map(|name| match target.operation_from_name(name) { + Ok(raw_op) => match raw_op.operation.view() { + OperationRef::Standard(_) | OperationRef::Gate(_) => Some((*name, raw_op.clone())), + _ => None, + }, + Err(_) => None, + }) + .collect(); + + let single_qubit_basis_list = + target.operation_names_for_qargs(Some(&smallvec![physical_qubits[0]])); + let mut target_basis_set = EulerBasisSet::new(); + match single_qubit_basis_list { + Ok(basis_list) => { + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)); + } + Err(_) => target_basis_set.support_all(), + } + if target_basis_set.basis_supported(EulerBasis::U3) + && target_basis_set.basis_supported(EulerBasis::U321) + { + target_basis_set.remove(EulerBasis::U3); + } + if target_basis_set.basis_supported(EulerBasis::ZSX) + && target_basis_set.basis_supported(EulerBasis::ZSXX) + { + target_basis_set.remove(EulerBasis::ZSX); + } + + let euler_bases: Vec = target_basis_set.get_bases().collect(); + + available_kak_gate + .iter() + .filter_map(|(two_qubit_name, two_qubit_gate)| { + let matrix = two_qubit_gate.matrix(); + matrix.map(|matrix| { + euler_bases.iter().map(move |euler_basis| { + TwoQubitBasisDecomposer::new_inner( + two_qubit_name.to_string(), + matrix.view(), + fidelity, + *euler_basis, + None, + ) + }) + }) + }) + .flatten() + .collect() +} + +#[inline] +fn score_sequence<'a>( + target: &'a Target, + kak_gate_name: &str, + sequence: impl Iterator, SmallVec<[Qubit; 2]>)> + 'a, +) -> f64 { + 1. - sequence + .map(|(gate, local_qubits)| { + let qubits = local_qubits + .iter() + .map(|qubit| PhysicalQubit(qubit.0)) + .collect::>(); + let name = match gate.as_ref() { + Some(g) => g.name(), + None => kak_gate_name, + }; + 1. - target.get_error(name, qubits.as_slice()).unwrap_or(0.) + }) + .product::() +} + +type MappingIterItem = Option<((TwoQubitGateSequence, String), [Qubit; 2])>; + +/// This transpiler pass can only run in a context where we've translated the circuit gates (or +/// where we know all gates have a matrix). If any gate identified in the run fails to have a +/// matrix defined (either in rust or python) it will be skipped +#[pyfunction] +pub(crate) fn two_qubit_unitary_peephole_optimize( + py: Python, + dag: &DAGCircuit, + target: &Target, + fidelity: f64, +) -> PyResult { + let runs: Vec> = dag.collect_2q_runs().unwrap(); + let node_mapping: HashMap = + HashMap::with_capacity(runs.iter().map(|run| run.len()).sum()); + let locked_node_mapping = Mutex::new(node_mapping); + + // Build a vec of all the best synthesized two qubit gate sequences from the collected runs. + // This is done in parallel + let run_mapping: PyResult> = runs + .par_iter() + .enumerate() + .map(|(run_index, node_indices)| { + let block_qubit_map = node_indices + .iter() + .filter_map(|node_index| { + let NodeType::Operation(ref inst) = dag.dag()[*node_index] else { + unreachable!("All run nodes will be ops") + }; + let qubits = dag.get_qargs(inst.qubits); + if qubits.len() == 2 { + Some(qubits) + } else { + None + } + }) + .next() + .map(|qubits| { + if qubits[0] > qubits[1] { + [qubits[1], qubits[0]] + } else { + [qubits[0], qubits[1]] + } + }) + .unwrap(); + let matrix = compose_2q_matrix(node_indices.iter().map(|node_index| { + let NodeType::Operation(ref inst) = dag.dag()[*node_index] else { + unreachable!("All run nodes will be ops") + }; + let op_matrix = inst.op.matrix(inst.params_view()).unwrap(); + let qubits = dag.get_qargs(inst.qubits); + let qubit_indices: SmallVec<[u8; 2]> = qubits + .iter() + .map(|qubit| if *qubit == block_qubit_map[0] { 0 } else { 1 }) + .collect(); + Ok((op_matrix, qubit_indices)) + }))?; + + let decomposers = get_decomposers_from_target(target, &block_qubit_map, fidelity)?; + let mut decomposer_scores: Vec> = vec![None; decomposers.len()]; + + let order_sequence = + |(index_a, sequence_a): &(usize, (TwoQubitGateSequence, String)), + (index_b, sequence_b): &(usize, (TwoQubitGateSequence, String))| { + let score_a = match decomposer_scores[*index_a] { + Some(score) => score, + None => { + let score: f64 = + score_sequence( + target, + sequence_a.1.as_str(), + sequence_a.0.gates.iter().map( + |(gate, _params, local_qubits)| { + let qubits = local_qubits + .iter() + .map(|qubit| block_qubit_map[*qubit as usize]) + .collect(); + (*gate, qubits) + }, + ), + ); + decomposer_scores[*index_a] = Some(score); + score + } + }; + + let score_b = match decomposer_scores[*index_b] { + Some(score) => score, + None => { + let score: f64 = + score_sequence( + target, + sequence_b.1.as_str(), + sequence_b.0.gates.iter().map( + |(gate, _params, local_qubits)| { + let qubits = local_qubits + .iter() + .map(|qubit| block_qubit_map[*qubit as usize]) + .collect(); + (*gate, qubits) + }, + ), + ); + decomposer_scores[*index_b] = Some(score); + score + } + }; + score_a.partial_cmp(&score_b).unwrap_or(Ordering::Equal) + }; + + let sequence = decomposers + .iter() + .map(|decomposer| { + ( + decomposer + .call_inner(matrix.view(), None, true, None) + .unwrap(), + decomposer.gate_name().to_string(), + ) + }) + .enumerate() + .min_by(order_sequence) + .unwrap() + .1; + let original_score = 1. + - node_indices + .iter() + .map(|node_index| { + let NodeType::Operation(ref inst) = dag.dag()[*node_index] else { + unreachable!("All run nodes will be ops") + }; + let qubits = dag + .get_qargs(inst.qubits) + .iter() + .map(|qubit| PhysicalQubit(qubit.0)) + .collect::>(); + let name = inst.op.name(); + 1. - target.get_error(name, qubits.as_slice()).unwrap_or(0.) + }) + .product::(); + let new_score = score_sequence( + target, + sequence.1.as_str(), + sequence + .0 + .gates + .iter() + .map(|(gate, _params, local_qubits)| { + let qubits = local_qubits + .iter() + .map(|qubit| block_qubit_map[*qubit as usize]) + .collect(); + (*gate, qubits) + }), + ); + + if new_score > original_score + || (new_score == original_score + && sequence + .0 + .gates + .iter() + .filter(|(_, __, qubits)| qubits.len() == 2) + .count() + >= node_indices + .iter() + .filter(|node_index| { + let NodeType::Operation(ref inst) = dag.dag()[**node_index] else { + unreachable!("All run nodes will be ops") + }; + let qubits = dag.get_qargs(inst.qubits); + qubits.len() == 2 + }) + .count()) + { + return Ok(None); + } + // This is done at the end of the map in some attempt to minimize + // lock contention. If this were serial code it'd make more sense + // to do this as part of the iteration building the + let mut node_mapping = locked_node_mapping.lock().unwrap(); + for node in node_indices { + node_mapping.insert(*node, run_index); + } + Ok(Some((sequence, block_qubit_map))) + }) + .collect(); + + let run_mapping = run_mapping?; + // After we've computed all the sequences to execute now serially build up a new dag. + let mut processed_runs: HashSet = HashSet::with_capacity(run_mapping.len()); + let mut out_dag = dag.copy_empty_like(py, "alike")?; + let node_mapping = locked_node_mapping.into_inner().unwrap(); + for node in dag.topological_op_nodes()? { + match node_mapping.get(&node) { + Some(run_index) => { + if processed_runs.contains(run_index) { + continue; + } + if run_mapping[*run_index].is_none() { + let NodeType::Operation(ref instr) = dag.dag()[node] else { + unreachable!("Must be an op node") + }; + out_dag.push_back(py, instr.clone())?; + continue; + } + let (sequence, qubit_map) = &run_mapping[*run_index].as_ref().unwrap(); + for (gate, params, local_qubits) in &sequence.0.gates { + let qubits: Vec = local_qubits + .iter() + .map(|index| qubit_map[*index as usize]) + .collect(); + let out_params = if params.is_empty() { + None + } else { + Some(params.iter().map(|val| Param::Float(*val)).collect()) + }; + match gate { + Some(gate) => { + #[cfg(feature = "cache_pygates")] + { + out_dag.apply_operation_back( + py, + PackedOperation::from_standard(*gate), + qubits.as_slice(), + &[], + out_params, + ExtraInstructionAttributes::default(), + None, + ) + } + #[cfg(not(feature = "cache_pygates"))] + { + out_dag.apply_operation_back( + py, + PackedOperation::from_standard(*gate), + qubits.as_slice(), + &[], + out_params, + ExtraInstructionAttributes::default(), + ) + } + } + None => { + let gate = target.operation_from_name(sequence.1.as_str()).unwrap(); + #[cfg(feature = "cache_pygates")] + { + out_dag.apply_operation_back( + py, + gate.operation.clone(), + qubits.as_slice(), + &[], + out_params, + ExtraInstructionAttributes::default(), + None, + ) + } + #[cfg(not(feature = "cache_pygates"))] + { + out_dag.apply_operation_back( + py, + gate.operation.clone(), + qubits.as_slice(), + &[], + out_params, + ExtraInstructionAttributes::default(), + ) + } + } + }?; + } + out_dag.add_global_phase(py, &Param::Float(sequence.0.global_phase))?; + processed_runs.insert(*run_index); + } + None => { + let NodeType::Operation(ref instr) = dag.dag()[node] else { + unreachable!("Must be an op node") + }; + out_dag.push_back(py, instr.clone())?; + } + } + } + Ok(out_dag) +} + +pub fn two_qubit_peephole_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(two_qubit_unitary_peephole_optimize))?; + Ok(()) +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 54f270b30558..74232a4555bd 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -1558,7 +1558,7 @@ def _format(operand): /// Returns: /// DAGCircuit: An empty copy of self. #[pyo3(signature = (*, vars_mode="alike"))] - fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { + pub fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { let mut target_dag = DAGCircuit::with_capacity( py, self.num_qubits(), @@ -5074,7 +5074,7 @@ impl DAGCircuit { /// This is mostly used to apply operations from one DAG to /// another that was created from the first via /// [DAGCircuit::copy_empty_like]. - fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { + pub fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { let op_name = instr.op.name(); let (all_cbits, vars): (Vec, Option>) = { if self.may_have_additional_wires(py, &instr) { diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index 413165e84b1f..eccc3ba8a87a 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib"] # crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the # feature a default, and run `cargo test --no-default-features` to turn it off. default = ["pyo3/extension-module"] -cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"] +cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates", "qiskit-accelerate/cache_pygates"] [dependencies] pyo3.workspace = true diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 6033c7c47e49..77cc584de05f 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -80,5 +80,10 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, commutation_checker, "commutation_checker")?; add_submodule(m, commutation_analysis, "commutation_analysis")?; add_submodule(m, commutation_cancellation, "commutation_cancellation")?; + add_submodule( + m, + qiskit_accelerate::two_qubit_peephole::two_qubit_peephole_mod, + "two_qubit_peephole", + )?; Ok(()) } diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 25137d7a5918..cf411854fe0f 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -91,6 +91,7 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.two_qubit_peephole"] = _accelerate.two_qubit_peephole from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/optimization/two_qubit_peephole.py b/qiskit/transpiler/passes/optimization/two_qubit_peephole.py new file mode 100644 index 000000000000..4e218773d367 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/two_qubit_peephole.py @@ -0,0 +1,57 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Splits each two-qubit gate in the `dag` into two single-qubit gates, if possible without error.""" + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passes.optimization import Collect2qBlocks, ConsolidateBlocks +from qiskit.transpiler.passes.synthesis import UnitarySynthesis +from qiskit.transpiler.target import Target +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit._accelerate.two_qubit_peephole import two_qubit_unitary_peephole_optimize + + +class TwoQubitPeepholeOptimization(TransformationPass): + """Unified two qubit unitary peephole optimization""" + + def __init__( + self, + target: Target, + approximation_degree: float | None = 1.0, + method: str = "default", + plugin_config: dict = None, + ): + super().__init__() + self._target = target + self._approximation_degree = approximation_degree + self._pm = None + if method != "default": + self._pm = PassManager( + [ + Collect2qBlocks(), + ConsolidateBlocks( + target=self._target, approximation_degree=self._approximation_degree + ), + UnitarySynthesis( + target=target, + approximation_degree=approximation_degree, + method=method, + plugin_config=plugin_config, + ), + ] + ) + + def run(self, dag: DAGCircuit) -> DAGCircuit: + if self._pm is not None: + return self._pm.run(dag) + return two_qubit_unitary_peephole_optimize(dag, self._target, self._approximation_degree)