From 104ac8d08abb3437c3f82d6ef7b9ffc6d13c11ec Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 18 Jul 2025 15:00:02 -0400 Subject: [PATCH 01/22] Add transpiler C API This commit adds the initial transpiler C API to Qiskit, it provides a function for running the transpiler to compile a single circuit for a provided target. This function is not designed to replace the Python transpiler and should only be used with circuits defined solely using the C API. It is backed by the same Rust implementation of the trnaspiler passes, but when coming from the C/Rust API several assumptions are made that the circuits only contain the feature set defined in the C API and the results are only valid when called with circuits built solely using the C API. As this function is the initial version of the transpiler in C/Rust there are still a few limitations beyond just the circuits come from C and only have the features exposed to C. The first is that multi qubit unitary synthesis does not exist in C yet, so if you construct any 3 or more qubit UnitaryGates the transpiler will error. Additionally, there is no scheduling stage yet in the C transpiler yet, so there are no options for scheduling circuits available. Finally, there is no pluggability to the transpiler, unlike Python where the preset pass managers and the transpile() function provide a great deal of configurability and customization via composing your own passes or using the plugin interface, these concepts do no exist in the C transpiler. The limitations will be addressed in future releases, but this first function is about enabling initial circuit compilation support from C. --- Cargo.lock | 7 + crates/cext/cbindgen.toml | 2 + crates/cext/src/transpiler/mod.rs | 1 + .../cext/src/transpiler/transpile_function.rs | 181 +++++ crates/circuit/src/dag_circuit.rs | 4 +- crates/transpiler/Cargo.toml | 1 + crates/transpiler/src/lib.rs | 4 + crates/transpiler/src/passes/sabre/mod.rs | 5 +- crates/transpiler/src/passes/sabre/route.rs | 2 +- crates/transpiler/src/transpile_layout.rs | 4 +- crates/transpiler/src/transpiler.rs | 659 ++++++++++++++++++ test/c/test_transpiler.c | 163 +++++ 12 files changed, 1027 insertions(+), 6 deletions(-) create mode 100644 crates/cext/src/transpiler/transpile_function.rs create mode 100644 crates/transpiler/src/transpiler.rs create mode 100644 test/c/test_transpiler.c diff --git a/Cargo.lock b/Cargo.lock index dc7450dc1daa..0d9a0e6a16a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + [[package]] name = "approx" version = "0.5.1" @@ -1636,6 +1642,7 @@ name = "qiskit-transpiler" version = "2.2.0" dependencies = [ "ahash 0.8.12", + "anyhow", "approx", "bytemuck", "fixedbitset 0.5.7", diff --git a/crates/cext/cbindgen.toml b/crates/cext/cbindgen.toml index 6c9bbe18e693..502e3aa51d72 100644 --- a/crates/cext/cbindgen.toml +++ b/crates/cext/cbindgen.toml @@ -81,3 +81,5 @@ prefix_with_name = true "VF2LayoutResult" = "QkVF2LayoutResult" "ElidePermutationsResult" = "QkElidePermutationsResult" "TranspileLayout" = "QkTranspileLayout" +"TranspileResult" = "QkTranspileResult" +"TranspileOptions" = "QkTranspileOptions" diff --git a/crates/cext/src/transpiler/mod.rs b/crates/cext/src/transpiler/mod.rs index 75d02ebd43e0..ee406c2e6c8f 100644 --- a/crates/cext/src/transpiler/mod.rs +++ b/crates/cext/src/transpiler/mod.rs @@ -12,4 +12,5 @@ pub mod passes; pub mod target; +pub mod transpile_function; pub mod transpile_layout; diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs new file mode 100644 index 000000000000..de077e9bea00 --- /dev/null +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -0,0 +1,181 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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::ffi::c_char; +use std::ffi::CString; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_transpiler::target::Target; + +use qiskit_transpiler::transpile; +use qiskit_transpiler::transpile_layout::TranspileLayout; + +use crate::pointers::const_ptr_as_ref; + +/// @ingroup QkTranspiler +/// The container result object from ``qk_transpile`` +/// +/// When the transpiler successfully compiles a quantum circuit for a given target it +/// returns the transpiled circuit and the layout. +#[repr(C)] +pub struct TranspileResult { + circuit: *mut CircuitData, + layout: *mut TranspileLayout, +} + +/// The options for running the transpiler +#[repr(C)] +pub struct TranspileOptions { + /// The optimization level to run the transpiler with. Valid values are 0, 1, 2, or 3. + optimization_level: u8, + /// The seed for the transpiler. If set to a negative number this means no seed will be + /// set and the RNGs used in the transpiler will be seeded froms system entropy. + seed: i64, + /// The approximation degree a heurstic dial where 1.0 means no approximation (up to numerical + /// tolerance) and 0.0 means the maximum approximation. A `NAN` value indicates that + /// approximation is allowed up to the reported error rate for an operation in the target. + approximation_degree: f64, +} + +impl Default for TranspileOptions { + fn default() -> Self { + TranspileOptions { + optimization_level: 2, + seed: -1, + approximation_degree: 1.0, + } + } +} + +/// @ingroup QkTranspiler +/// +/// Generate transpiler options defaults +/// +/// This function generates a QkTranspileOptions with the default settings +/// This currently is ``optimization_level`` 2, no seed, and no approximation. +#[no_mangle] +#[cfg(feature = "cbinding")] +pub extern "C" fn qk_transpiler_default_options() -> TranspileOptions { + TranspileOptions::default() +} + +/// @ingroup QkTranspiler +/// Transpile a single circuit that was constructed using the C API +/// +/// The Qiskit transpiler is a quantum circuit compiler that rewrites a given +/// input circuit to match the constraints of a QPU and/or optimize the circuit +/// for execution. +/// +/// This function is multithreaded internally and will launch a thread pool +/// with threads equal to the number of CPUs by default. You can tune the +/// number of threads with the ``RAYON_NUM_THREADS`` environment variable. +/// For example, setting ``RAYON_NUM_THREADS=4`` would limit the thread pool +/// to 4 threads. +/// +/// @param circuit A pointer to the circuit to run the transpiler on +/// @param target A pointer to the target to compile the circuit for +/// @params options A pointer to an options object that define user options if this is a null +/// pointer the default values will be used +/// @param result A pointer to the memory location of the transpiler result. On a successful +/// execution (return code 0) the output of the transpiler will be written to the pointer +/// @param error A pointer to a pointer with an nul terminated string with an error description. +/// If the transpiler fails a pointer to the string with the error description will be written +/// to this pointer. That pointer needs to be freed with `qk_str_free`. +/// +/// @returns the return code for the transpiler, 0 means success and all other values indicate an +/// error +/// +/// # Safety +/// +/// Behavior is undefined if ``circuit``, ``target``, ``options``, ``result``, or ``error`` are +/// not valid, non-null pointers to a ``QkCircuit``, ``QkTarget``, ``QkTranspileOptions``, +/// ``QkTranspileResult``, or a ``char`` pointer respectively. +#[no_mangle] +#[cfg(feature = "cbinding")] +pub unsafe extern "C" fn qk_transpile( + qc: *const CircuitData, + target: *const Target, + options: *const TranspileOptions, + result: *mut TranspileResult, + error: *mut *mut c_char, +) -> i32 { + // SAFETY: Per documentation, the pointer is non-null and aligned. + let qc = unsafe { const_ptr_as_ref(qc) }; + let target = unsafe { const_ptr_as_ref(target) }; + let options = unsafe { const_ptr_as_ref(options) }; + if ![0, 1, 2, 3].contains(&options.optimization_level) { + panic!( + "Invalid optimization level specified {}", + options.optimization_level + ); + } + let seed = if options.seed < 0 { + None + } else { + Some(options.seed as u64) + }; + let approximation_degree = if options.approximation_degree.is_nan() { + None + } else { + if !(0.0..=1.0).contains(&options.approximation_degree) { + panic!("Invalid value provided for approximation degree, only NAN or values between 0.0 and 1.0 inclusive are valid"); + } + Some(options.approximation_degree) + }; + + if let Some(target_qubits) = target.num_qubits { + if target_qubits < qc.num_qubits() as u32 { + unsafe { + *error = CString::new(format!( + "Insufficient qubits in target: {}, the circuit uses {}", + target_qubits, + qc.num_qubits() + )) + .unwrap() + .into_raw(); + } + return 1; + } + } + + match transpile( + qc, + target, + options.optimization_level, + approximation_degree, + seed, + ) { + Ok(transpile_result) => { + unsafe { + *result = TranspileResult { + circuit: Box::into_raw(Box::new(transpile_result.0)), + layout: Box::into_raw(Box::new(transpile_result.1)), + }; + } + 0 + } + Err(e) => { + unsafe { + // Right now we return a backtrace of the error. This at least gives a hint as to + // which pass failed when we have rust errors normalized we can actually have error + // messages which are user facing. But most likely this will be a PyErr and panic + // when trying to extract the string. + *error = CString::new(format!( + "Transpilation failed with this backtrace: {}", + e.backtrace() + )) + .unwrap() + .into_raw(); + } + 1 + } + } +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 054bcbf06771..eaabd9b09cc7 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -1715,7 +1715,7 @@ impl DAGCircuit { /// DAGCircuitError: if an unknown :class:`.ControlFlowOp` is present in a call with /// ``recurse=True``, or any control flow is present in a non-recursive call. #[pyo3(signature= (*, recurse=false))] - fn size(&self, recurse: bool) -> PyResult { + pub fn size(&self, recurse: bool) -> PyResult { let mut length = self.num_ops(); if !self.has_control_flow() { return Ok(length); @@ -1790,7 +1790,7 @@ impl DAGCircuit { /// DAGCircuitError: if unknown control flow is present in a recursive call, or any control /// flow is present in a non-recursive call. #[pyo3(signature= (*, recurse=false))] - fn depth(&self, recurse: bool) -> PyResult { + pub fn depth(&self, recurse: bool) -> PyResult { if self.qubits.is_empty() && self.clbits.is_empty() && self.num_vars() == 0 { return Ok(0); } diff --git a/crates/transpiler/Cargo.toml b/crates/transpiler/Cargo.toml index 9e18e9446fd0..910171f0d1ef 100644 --- a/crates/transpiler/Cargo.toml +++ b/crates/transpiler/Cargo.toml @@ -29,6 +29,7 @@ qiskit-quantum-info.workspace = true thiserror.workspace = true bytemuck.workspace = true fixedbitset = "0.5.7" +anyhow = "1.0" [dependencies.uuid] workspace = true diff --git a/crates/transpiler/src/lib.rs b/crates/transpiler/src/lib.rs index 5a2faa499f09..aaa81541b843 100644 --- a/crates/transpiler/src/lib.rs +++ b/crates/transpiler/src/lib.rs @@ -18,6 +18,10 @@ pub mod standard_gates_commutations; pub mod target; pub mod transpile_layout; +mod transpiler; + +pub use transpiler::transpile; + mod gate_metrics; use pyo3::import_exception_bound; diff --git a/crates/transpiler/src/passes/sabre/mod.rs b/crates/transpiler/src/passes/sabre/mod.rs index e3e0f8c82cd9..ff178c9f87f3 100644 --- a/crates/transpiler/src/passes/sabre/mod.rs +++ b/crates/transpiler/src/passes/sabre/mod.rs @@ -16,12 +16,15 @@ pub mod heuristic; mod layer; mod layout; mod neighbors; -mod route; +pub(crate) mod route; use pyo3::prelude::*; use pyo3::wrap_pyfunction; +pub(crate) use heuristic::Heuristic; +pub(crate) use heuristic::SetScaling; pub use layout::sabre_layout_and_routing; +pub(crate) use route::sabre_routing; pub fn sabre(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(route::sabre_routing))?; diff --git a/crates/transpiler/src/passes/sabre/route.rs b/crates/transpiler/src/passes/sabre/route.rs index 1fb8e73cc245..b417625ae3f3 100644 --- a/crates/transpiler/src/passes/sabre/route.rs +++ b/crates/transpiler/src/passes/sabre/route.rs @@ -316,7 +316,7 @@ pub struct PyRoutingTarget(pub Option); #[pymethods] impl PyRoutingTarget { #[staticmethod] - fn from_target(target: &Target) -> PyResult { + pub(crate) fn from_target(target: &Target) -> PyResult { let coupling = match target.coupling_graph() { Ok(coupling) => coupling, Err(TargetCouplingError::AllToAll) => return Ok(Self(None)), diff --git a/crates/transpiler/src/transpile_layout.rs b/crates/transpiler/src/transpile_layout.rs index 03c285997392..b34f4dab2ee3 100644 --- a/crates/transpiler/src/transpile_layout.rs +++ b/crates/transpiler/src/transpile_layout.rs @@ -35,12 +35,12 @@ use qiskit_circuit::imports::TRANSPILE_LAYOUT; pub struct TranspileLayout { /// The initial layout which is mapping the virtual qubits in the input circuit to the /// transpiler to the physical qubits used on the transpilation target - initial_layout: Option, + pub initial_layout: Option, /// The optional routing permutation that /// represents the permutation caused by routing or permutation elision during /// transpilation. This vector maps the qubits at the start of the circuit to their /// final position/physical qubit at the end of the circuit. - output_permutation: Option>, + pub output_permutation: Option>, /// The virtual qubits [`ShareableQubit`] objects from the input circuit to the transpiler. /// This vec should be arranged in order as in the original circuit, the index of the `Vec` /// corresponds to the [`VirtualQubit`] in the `initial_layout` attribute. This should include diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs new file mode 100644 index 000000000000..7cdb6da028f6 --- /dev/null +++ b/crates/transpiler/src/transpiler.rs @@ -0,0 +1,659 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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 hashbrown::HashSet; + +use anyhow::Result; + +use crate::commutation_checker::get_standard_commutation_checker; +use crate::equivalence::EquivalenceLibrary; +use crate::passes::sabre::route::PyRoutingTarget; +use crate::passes::*; +use crate::standard_equivalence_library::generate_standard_equivalence_library; +use crate::target::Target; +use crate::transpile_layout::TranspileLayout; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::converters::dag_to_circuit; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::{PhysicalQubit, Qubit}; + +/// A transpilation function for Rust native circuits for use in the C API. This will not cover +/// things that only exist in the Python API such as custom gates or control flow. When those +/// concepts exist in the rust data model this function must be expanded before adding them to the +/// C API. +pub fn transpile( + circuit: &CircuitData, + target: &Target, + optimization_level: u8, + approximation_degree: Option, + seed: Option, +) -> Result<(CircuitData, TranspileLayout)> { + if !(0..=3u8).contains(&optimization_level) { + panic!("Invalid optimization level specified {optimization_level}"); + } + let mut dag = DAGCircuit::from_circuit_data(circuit, false, None, None, None, None)?; + let mut commutation_checker = get_standard_commutation_checker(); + let mut equivalence_library = generate_standard_equivalence_library(); + let mut transpile_layout: TranspileLayout = TranspileLayout::new( + None, + None, + dag.qubits().objects().to_owned(), + dag.num_qubits() as u32, + ); + + let unroll_3q_or_more = |dag: &mut DAGCircuit| -> Result<()> { + // This will panic if there is a 3q unitary until qsd is ported + let mut out_dag = run_unitary_synthesis( + dag, + (0..dag.num_qubits()).collect(), + 3, + Some(target), + HashSet::new(), + ["unitary".to_string(), "swap".to_string()] + .into_iter() + .collect(), + HashSet::new(), + approximation_degree, + None, + None, + false, + )?; + run_unroll_3q_or_more(&mut out_dag, Some(target))?; + *dag = out_dag; + Ok(()) + }; + + // Init stage + unroll_3q_or_more(&mut dag)?; + if optimization_level == 1 { + run_inverse_cancellation_standard_gates(&mut dag); + } else if optimization_level == 2 || optimization_level == 3 { + if let Some((new_dag, permutation)) = run_elide_permutations(&dag)? { + dag = new_dag; + let permutation: Vec = permutation.into_iter().map(Qubit::new).collect(); + transpile_layout.compose_output_permutation(&permutation, false); + }; + run_remove_diagonal_before_measure(&mut dag); + run_remove_identity_equiv(&mut dag, approximation_degree, Some(target)); + run_inverse_cancellation_standard_gates(&mut dag); + cancel_commutations(&mut dag, &mut commutation_checker, None, 1.0)?; + run_consolidate_blocks(&mut dag, false, approximation_degree, None)?; + let result = run_split_2q_unitaries( + &mut dag, + approximation_degree + .unwrap_or(1.0 - f64::EPSILON) + .min(1.0 - f64::EPSILON), + true, + )?; + if let Some(result) = result { + dag = result.0; + let split_2q_permutation = result.1.into_iter().map(Qubit::new).collect::>(); + transpile_layout.compose_output_permutation(&split_2q_permutation, true); + } + } + // layout stage + + let sabre_heuristic = sabre::Heuristic::new( + None, + None, + None, + target.num_qubits.map(|x| (x * 10) as usize), + 1e-10, + ) + .with_basic(1.0, sabre::SetScaling::Constant) + .with_lookahead(0.5, 20, sabre::SetScaling::Size) + .with_decay(0.001, 5)?; + + if optimization_level == 0 { + // Apply a trivial layout + apply_layout( + &mut dag, + &mut transpile_layout, + target.num_qubits.unwrap(), + |x| PhysicalQubit(x.0), + ); + } else if optimization_level == 1 { + if run_check_map(&dag, target).is_none() { + apply_layout( + &mut dag, + &mut transpile_layout, + target.num_qubits.unwrap(), + |x| PhysicalQubit(x.0), + ); + } else if let Some(vf2_result) = + vf2_layout_pass(&dag, target, false, Some(5_000_000), None, Some(2500), None)? + { + apply_layout( + &mut dag, + &mut transpile_layout, + target.num_qubits.unwrap(), + |x| vf2_result[&x], + ); + } else { + let (result, initial_layout, final_layout) = sabre::sabre_layout_and_routing( + &mut dag, + target, + &sabre_heuristic, + 2, + 20, + 20, + seed, + Vec::new(), + false, + )?; + dag = result; + transpile_layout.initial_layout = Some(initial_layout); + let permutation: Vec = final_layout + .iter_virtual() + .map(|(_, x)| Qubit(x.0)) + .collect(); + transpile_layout.compose_output_permutation(&permutation, true); + } + } else if optimization_level == 2 { + if let Some(vf2_result) = + vf2_layout_pass(&dag, target, false, Some(5_000_000), None, Some(2500), None)? + { + apply_layout( + &mut dag, + &mut transpile_layout, + target.num_qubits.unwrap(), + |x| vf2_result[&x], + ); + } else { + let (result, initial_layout, final_layout) = sabre::sabre_layout_and_routing( + &mut dag, + target, + &sabre_heuristic, + 2, + 20, + 20, + seed, + Vec::new(), + false, + )?; + dag = result; + transpile_layout.initial_layout = Some(initial_layout); + let permutation: Vec = final_layout + .iter_virtual() + .map(|(_, x)| Qubit(x.0)) + .collect(); + transpile_layout.compose_output_permutation(&permutation, true); + } + } else if let Some(vf2_result) = vf2_layout_pass( + &dag, + target, + false, + Some(30_000_000), + None, + Some(250_000), + None, + )? { + apply_layout( + &mut dag, + &mut transpile_layout, + target.num_qubits.unwrap(), + |x| vf2_result[&x], + ); + } else { + let (result, initial_layout, final_layout) = sabre::sabre_layout_and_routing( + &mut dag, + target, + &sabre_heuristic, + 4, + 20, + 20, + seed, + Vec::new(), + false, + )?; + dag = result; + transpile_layout.initial_layout = Some(initial_layout); + let permutation: Vec = final_layout + .iter_virtual() + .map(|(_, x)| Qubit(x.0)) + .collect(); + transpile_layout.compose_output_permutation(&permutation, true); + } + // Routing stage + // let vf2_post_result = + if optimization_level == 0 { + let routing_target = PyRoutingTarget::from_target(target)?; + + if run_check_map(&dag, target).is_none() { + let (out_dag, final_layout) = sabre::sabre_routing( + &dag, + &routing_target, + &sabre_heuristic, + transpile_layout.initial_layout.as_ref().unwrap(), + 5, + seed, + Some(true), + )?; + dag = out_dag; + let routing_permutation: Vec = final_layout + .iter_virtual() + .map(|(_, x)| Qubit(x.0)) + .collect(); + transpile_layout.compose_output_permutation(&routing_permutation, true); + } + // None + } + // else if optimization_level == 1 { + // vf2_post_layout_pass(&dag, target, false, Some(50_000), None, Some(2_500), None).unwrap() + // } else if optimization_level == 2 { + // vf2_post_layout_pass(&dag, target, false, Some(50_000), None, Some(2_500), None).unwrap() + // } else { + // vf2_post_layout_pass( + // &dag, + // target, + // false, + // Some(30_000_000), + // None, + // Some(250_000), + // None, + // ) + // .unwrap() + // }; + // if let Some(post_layout) = vf2_post_result { + // update_layout(&mut dag, &mut transpile_layout, |qubit| { + // Qubit(post_layout[VirtualQubit(qubit.0)].0) + // }); + // } + // Translation Stage + let translation = |dag: &mut DAGCircuit, equiv_lib: &mut EquivalenceLibrary| -> Result<()> { + let num_qubits = dag.num_qubits(); + *dag = run_unitary_synthesis( + dag, + (0..num_qubits).collect(), + 0, + Some(target), + HashSet::new(), + ["unitary".to_string()].into_iter().collect(), + HashSet::new(), + approximation_degree, + None, + None, + false, + )?; + if let Some(out_dag) = run_basis_translator(dag, equiv_lib, 0, Some(target), None)? { + *dag = out_dag; + } + if !check_direction_target(dag, target)? { + fix_direction_target(dag, target)?; + if gates_missing_from_target(dag, target)? { + if let Some(out_dag) = + run_basis_translator(dag, equiv_lib, 0, Some(target), None).unwrap() + { + *dag = out_dag; + } + } + } + Ok(()) + }; + translation(&mut dag, &mut equivalence_library)?; + // optimization stage + let mut depth: Option = None; + let mut size: Option = None; + let mut new_depth; + let mut new_size; + if optimization_level == 1 { + new_depth = Some(dag.depth(false)?); + new_size = Some(dag.size(false)?); + while new_depth != depth || new_size != size { + depth = new_depth; + size = new_size; + run_optimize_1q_gates_decomposition(&mut dag, Some(target), None, None)?; + run_inverse_cancellation_standard_gates(&mut dag); + if gates_missing_from_target(&dag, target)? { + translation(&mut dag, &mut equivalence_library)?; + } + new_depth = Some(dag.depth(false)?); + new_size = Some(dag.size(false)?); + } + } else if optimization_level == 2 { + run_consolidate_blocks(&mut dag, false, approximation_degree, Some(target))?; + let num_qubits = dag.num_qubits(); + dag = run_unitary_synthesis( + &mut dag, + (0..num_qubits).collect(), + 0, + Some(target), + HashSet::new(), + ["unitary".to_string()].into_iter().collect(), + HashSet::new(), + approximation_degree, + None, + None, + false, + )?; + new_depth = Some(dag.depth(false)?); + new_size = Some(dag.size(false)?); + while new_depth != depth || new_size != size { + depth = new_depth; + size = new_size; + run_remove_identity_equiv(&mut dag, approximation_degree, Some(target)); + run_optimize_1q_gates_decomposition(&mut dag, Some(target), None, None)?; + cancel_commutations(&mut dag, &mut commutation_checker, None, 1.0)?; + if gates_missing_from_target(&dag, target)? { + translation(&mut dag, &mut equivalence_library)?; + } + new_depth = Some(dag.depth(false)?); + new_size = Some(dag.size(false)?); + } + } else if optimization_level == 3 { + struct MinPointState { + best_depth: Option, + best_size: Option, + count: usize, + best_dag: DAGCircuit, + } + let mut min_state = MinPointState { + best_depth: None, + best_size: None, + count: 0, + best_dag: dag.clone(), + }; + impl MinPointState { + fn check( + &mut self, + dag: &DAGCircuit, + new_size: Option, + new_depth: Option, + ) -> bool { + if self.best_depth.is_none() || self.best_size.is_none() { + self.best_depth = new_depth; + self.best_size = new_size; + self.best_dag = dag.clone(); + true + } else if (new_depth, new_size) > (self.best_depth, self.best_size) { + self.count += 1; + true + } else if (new_depth, new_size) < (self.best_depth, self.best_size) { + self.count = 1; + self.best_depth = new_depth; + self.best_size = new_size; + true + } else { + (new_depth, new_size) != (self.best_depth, self.best_size) + } + } + } + let mut continue_loop: bool = true; + while continue_loop { + run_consolidate_blocks(&mut dag, false, approximation_degree, Some(target))?; + let num_qubits = dag.num_qubits(); + dag = run_unitary_synthesis( + &mut dag, + (0..num_qubits).collect(), + 0, + Some(target), + HashSet::new(), + ["unitary"].into_iter().map(|x| x.to_string()).collect(), + HashSet::new(), + approximation_degree, + None, + None, + false, + )?; + run_remove_identity_equiv(&mut dag, approximation_degree, Some(target)); + run_optimize_1q_gates_decomposition(&mut dag, Some(target), None, None)?; + cancel_commutations(&mut dag, &mut commutation_checker, None, 1.0)?; + if gates_missing_from_target(&dag, target)? { + translation(&mut dag, &mut equivalence_library)?; + } + new_depth = Some(dag.depth(false)?); + new_size = Some(dag.size(false)?); + continue_loop = min_state.check(&dag, new_size, new_depth); + } + dag = min_state.best_dag; + } + Ok((dag_to_circuit(&dag, false)?, transpile_layout)) +} + +#[cfg(all(test, not(miri)))] +mod tests { + use super::*; + use crate::target::InstructionProperties; + use crate::target::Target; + use qiskit_circuit::circuit_data::CircuitData; + use qiskit_circuit::operations::{Operation, Param, StandardGate, StandardInstruction}; + use qiskit_circuit::parameter::parameter_expression::ParameterExpression; + use qiskit_circuit::parameter::symbol_expr::Symbol; + use qiskit_circuit::{Clbit, PhysicalQubit, Qubit}; + use smallvec::smallvec; + use std::sync::Arc; + + fn build_universal_star_target() -> Target { + let mut target = Target::default(); + let u_params = [ + Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( + "a", None, None, + )))), + Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( + "b", None, None, + )))), + Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( + "c", None, None, + )))), + ]; + + let props = (0..5) + .map(|i| { + ( + [PhysicalQubit(i)].into(), + Some(InstructionProperties::new( + Some(i as f64 * 1.2e-6), + Some(i as f64 * 2.3e-5), + )), + ) + }) + .collect(); + target + .add_instruction(StandardGate::U.into(), &u_params, None, Some(props)) + .unwrap(); + let props = (0..5) + .map(|i| { + ( + [PhysicalQubit(i)].into(), + Some(InstructionProperties::new( + Some(i as f64 * 1.2e-4), + Some(i as f64 * 2.3e-3), + )), + ) + }) + .collect(); + target + .add_instruction(StandardInstruction::Measure.into(), &[], None, Some(props)) + .unwrap(); + let props = (1..5) + .map(|i| { + ( + [PhysicalQubit(0), PhysicalQubit(i)].into(), + Some(InstructionProperties::new( + Some(i as f64 * 2.4e-6), + Some(i as f64 * 6.2e-4), + )), + ) + }) + .collect(); + target + .add_instruction(StandardGate::CZ.into(), &[], None, Some(props)) + .unwrap(); + target + } + + #[test] + fn test_bell_basic() { + let target = build_universal_star_target(); + let qc = CircuitData::from_packed_operations( + 2, + 2, + [ + Ok((StandardGate::H.into(), smallvec![], vec![Qubit(0)], vec![])), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(0), Qubit(1)], + vec![], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(0)], + vec![Clbit(0)], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(1)], + vec![Clbit(1)], + )), + ], + Param::Float(0.), + ) + .unwrap(); + for opt_level in 1..=3 { + let result = transpile(&qc, &target, opt_level, Some(1.0), Some(42)).unwrap(); + for inst in result.0.data() { + if inst.op.num_qubits() == 2 { + assert_eq!("cz", inst.op.name()); + target.contains_qargs( + &result + .0 + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit(x.0)) + .collect::>(), + ); + } else if inst.op.num_clbits() == 1 { + assert_eq!("measure", inst.op.name()); + } else { + assert_eq!("u", inst.op.name()); + } + } + assert!(result.1.output_permutation().is_none()); + } + } + + #[test] + fn test_routing_circuit() { + let target = build_universal_star_target(); + let qc = CircuitData::from_packed_operations( + 5, + 5, + [ + Ok((StandardGate::H.into(), smallvec![], vec![Qubit(0)], vec![])), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(0), Qubit(1)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(0), Qubit(2)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(0), Qubit(3)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(0), Qubit(4)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(4), Qubit(1)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(4), Qubit(2)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(4), Qubit(3)], + vec![], + )), + Ok(( + StandardGate::CX.into(), + smallvec![], + vec![Qubit(4), Qubit(0)], + vec![], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(0)], + vec![Clbit(0)], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(1)], + vec![Clbit(1)], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(2)], + vec![Clbit(2)], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(3)], + vec![Clbit(3)], + )), + Ok(( + StandardInstruction::Measure.into(), + smallvec![], + vec![Qubit(4)], + vec![Clbit(4)], + )), + ], + Param::Float(0.), + ) + .unwrap(); + for opt_level in 1..=3 { + let result = transpile(&qc, &target, opt_level, Some(1.0), Some(42)).unwrap(); + for inst in result.0.data() { + if inst.op.num_qubits() == 2 { + assert_eq!("cz", inst.op.name()); + target.contains_qargs( + &result + .0 + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit(x.0)) + .collect::>(), + ); + } else if inst.op.num_clbits() == 1 { + assert_eq!("measure", inst.op.name()); + } else { + assert_eq!("u", inst.op.name()); + } + } + assert!(result.1.output_permutation().is_some()); + } + } +} diff --git a/test/c/test_transpiler.c b/test/c/test_transpiler.c new file mode 100644 index 000000000000..2bdc4b9dc3f1 --- /dev/null +++ b/test/c/test_transpiler.c @@ -0,0 +1,163 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025. +// +// 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. + +#include "common.h" +#include +#include +#include +#include +#include +#include +#include +#include + +int test_transpile_bv(void) { + const uint32_t num_qubits = 10; + QkTarget *target = qk_target_new(num_qubits); + int result = Ok; + + QkTargetEntry *x_entry = qk_target_entry_new(QkGate_X); + for (uint32_t i = 0; i < num_qubits; i++) { + uint32_t qargs[1] = { + i, + }; + double error = 0.8e-6 * (i + 1); + double duration = 1.8e-9 * (i + 1); + qk_target_entry_add_property(x_entry, qargs, 1, duration, error); + } + qk_target_add_instruction(target, x_entry); + + QkTargetEntry *sx_entry = qk_target_entry_new(QkGate_SX); + for (uint32_t i = 0; i < num_qubits; i++) { + uint32_t qargs[1] = { + i, + }; + double error = 0.8e-6 * (i + 1); + double duration = 1.8e-9 * (i + 1); + qk_target_entry_add_property(sx_entry, qargs, 1, duration, error); + } + qk_target_add_instruction(target, sx_entry); + + QkTargetEntry *rz_entry = qk_target_entry_new(QkGate_RZ); + for (uint32_t i = 0; i < num_qubits; i++) { + uint32_t qargs[1] = { + i, + }; + double error = 0.; + double duration = 0.; + qk_target_entry_add_property(rz_entry, qargs, 1, duration, error); + } + qk_target_add_instruction(target, rz_entry); + + QkTargetEntry *ecr_entry = qk_target_entry_new(QkGate_ECR); + for (uint32_t i = 0; i < num_qubits - 1; i++) { + uint32_t qargs[2] = {i, i + 1}; + double inst_error = 0.0090393 * (num_qubits - i); + double inst_duration = 0.020039; + + qk_target_entry_add_property(ecr_entry, qargs, 2, inst_duration, inst_error); + } + qk_target_add_instruction(target, ecr_entry); + + QkCircuit *qc = qk_circuit_new(num_qubits, 0); + uint32_t x_qargs[1] = { + 9, + }; + qk_circuit_gate(qc, QkGate_X, x_qargs, NULL); + for (uint32_t i = 0; i < qk_circuit_num_qubits(qc); i++) { + uint32_t qargs[1] = { + i, + }; + qk_circuit_gate(qc, QkGate_H, qargs, NULL); + } + for (uint32_t i = 0; i < qk_circuit_num_qubits(qc) - 1; i += 2) { + uint32_t qargs[2] = {i, 9}; + qk_circuit_gate(qc, QkGate_CX, qargs, NULL); + } + QkTranspileResult transpile_result = {NULL, NULL}; + char *error = NULL; + QkTranspileOptions options = qk_transpiler_default_options(); + options.seed = 42; + int result_code = qk_transpile(qc, target, &options, &transpile_result, &error); + if (result_code != 0) { + printf("Transpilation failed with: %s\n", error); + result = EqualityError; + goto circuit_cleanup; + } + + QkOpCounts op_counts = qk_circuit_count_ops(transpile_result.circuit); + if (op_counts.len != 4) { + printf("More than 4 types of gates in circuit, circuit's instructions are:\n"); + print_circuit(transpile_result.circuit); + result = EqualityError; + goto transpile_cleanup; + } + for (uint32_t i = 0; i < op_counts.len; i++) { + int sx_gate = strcmp(op_counts.data[i].name, "sx"); + int ecr_gate = strcmp(op_counts.data[i].name, "ecr"); + int x_gate = strcmp(op_counts.data[i].name, "x"); + int rz_gate = strcmp(op_counts.data[i].name, "rz"); + if (sx_gate != 0 && ecr_gate != 0 && x_gate != 0 && rz_gate != 0) { + printf("Gate type of %s found in the circuit which isn't expected\n", + op_counts.data[i].name); + result = EqualityError; + goto transpile_cleanup; + } + } + QkCircuitInstruction inst; + for (size_t i = 0; i < qk_circuit_num_instructions(transpile_result.circuit); i++) { + qk_circuit_get_instruction(transpile_result.circuit, i, &inst); + if (strcmp(inst.name, "ecr") == 0) { + if (inst.num_qubits != 2) { + printf("Unexpected number of qubits for ecr: %d\n", inst.num_qubits); + result = EqualityError; + qk_circuit_instruction_clear(&inst); + goto transpile_cleanup; + } + bool valid = false; + for (uint32_t qubit = 0; qubit < num_qubits - 1; qubit++) { + if (inst.qubits[0] == qubit && inst.qubits[1] == qubit + 1) { + valid = true; + break; + } + } + if (valid == false) { + printf("ECR Gate outside target on qubits: {%u, %u}\n", inst.qubits[0], + inst.qubits[1]); + result = EqualityError; + qk_circuit_instruction_clear(&inst); + goto transpile_cleanup; + } + } + qk_circuit_instruction_clear(&inst); + } + +transpile_cleanup: + qk_circuit_free(transpile_result.circuit); + qk_transpile_layout_free(transpile_result.layout); + qk_opcounts_free(op_counts); + +circuit_cleanup: + qk_circuit_free(qc); + qk_target_free(target); + return result; +} + +int test_transpiler(void) { + int num_failed = 0; + num_failed += RUN_TEST(test_transpile_bv); + + fflush(stderr); + fprintf(stderr, "=== Number of failed subtests: %i\n", num_failed); + + return num_failed; +} From 4ad3154f4fa58fba400efe548591ecb36c74596e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 26 Aug 2025 11:01:41 -0400 Subject: [PATCH 02/22] Add docs files --- docs/cdoc/index.h | 4 ++++ docs/cdoc/index.rst | 12 +++++++++++- docs/cdoc/qk-transpiler.rst | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/cdoc/qk-transpiler.rst diff --git a/docs/cdoc/index.h b/docs/cdoc/index.h index dc7a4aebc291..1db14ca693e6 100644 --- a/docs/cdoc/index.h +++ b/docs/cdoc/index.h @@ -49,3 +49,7 @@ /** * @defgroup QkSabreLayoutOptions QkSabreLayoutOptions */ + +/** + * @defgroup QkTranspiler QkTranspiler + */ diff --git a/docs/cdoc/index.rst b/docs/cdoc/index.rst index 700ebe13de64..a4207b6588bf 100644 --- a/docs/cdoc/index.rst +++ b/docs/cdoc/index.rst @@ -61,10 +61,20 @@ results transpiling circuits from Python via the C API. .. toctree:: :maxdepth: 1 - version + qk-transpiler.rst qk-target qk-target-entry qk-transpile-layout qk-transpiler-passes qk-vf2-layout-result qk-sabre-layout-options + + +--------- +Utilities +--------- + +.. toctree:: + :maxdepth: 1 + + version diff --git a/docs/cdoc/qk-transpiler.rst b/docs/cdoc/qk-transpiler.rst new file mode 100644 index 000000000000..3956fcd19805 --- /dev/null +++ b/docs/cdoc/qk-transpiler.rst @@ -0,0 +1,25 @@ +============ +QkTranspiler +============ + +The ``qk_transpiler()`` function exposes the :mod:`qiskit.transpiler` to C. +The basic functionality is using the same underlying but the transpiler as +exposed to C has more limitations than what is exposed to Python. The transpiler +assumes a circuit built constructed using solely the C API and is intented to +work solely in the case of a standalone C API. It will potentially not work +correctly when in a mixed Python/C use case. If you're mixing C and Python you +should call the :py:func:`.generate_preset_pass_manager` or +:py:func:`.transpile` functions for those circuits. + +Data Types +========== + +.. doxygenstruct:: QkTranspileOptions + :members: + +Functions +========= + +.. doxygengroup:: QkTranspiler + :members: + :content-only: From d7ca189cd2636a48af918c073f0dce456994209a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 26 Aug 2025 12:12:19 -0400 Subject: [PATCH 03/22] Fix routing and level 0 --- crates/transpiler/src/transpiler.rs | 97 ++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index 7cdb6da028f6..296d366348c9 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -152,11 +152,22 @@ pub fn transpile( )?; dag = result; transpile_layout.initial_layout = Some(initial_layout); - let permutation: Vec = final_layout - .iter_virtual() - .map(|(_, x)| Qubit(x.0)) + let routing_permutation: Vec = (0..dag.num_qubits() as u32) + .map(|ref q| { + Qubit( + final_layout + .virtual_to_physical( + transpile_layout + .initial_layout + .as_ref() + .unwrap() + .physical_to_virtual(PhysicalQubit(*q)), + ) + .0, + ) + }) .collect(); - transpile_layout.compose_output_permutation(&permutation, true); + transpile_layout.compose_output_permutation(&routing_permutation, true); } } else if optimization_level == 2 { if let Some(vf2_result) = @@ -182,11 +193,22 @@ pub fn transpile( )?; dag = result; transpile_layout.initial_layout = Some(initial_layout); - let permutation: Vec = final_layout - .iter_virtual() - .map(|(_, x)| Qubit(x.0)) + let routing_permutation: Vec = (0..dag.num_qubits() as u32) + .map(|ref q| { + Qubit( + final_layout + .virtual_to_physical( + transpile_layout + .initial_layout + .as_ref() + .unwrap() + .physical_to_virtual(PhysicalQubit(*q)), + ) + .0, + ) + }) .collect(); - transpile_layout.compose_output_permutation(&permutation, true); + transpile_layout.compose_output_permutation(&routing_permutation, true); } } else if let Some(vf2_result) = vf2_layout_pass( &dag, @@ -217,18 +239,30 @@ pub fn transpile( )?; dag = result; transpile_layout.initial_layout = Some(initial_layout); - let permutation: Vec = final_layout - .iter_virtual() - .map(|(_, x)| Qubit(x.0)) + let routing_permutation: Vec = (0..dag.num_qubits() as u32) + .map(|ref q| { + Qubit( + final_layout + .virtual_to_physical( + transpile_layout + .initial_layout + .as_ref() + .unwrap() + .physical_to_virtual(PhysicalQubit(*q)), + ) + .0, + ) + }) .collect(); - transpile_layout.compose_output_permutation(&permutation, true); + + transpile_layout.compose_output_permutation(&routing_permutation, true); } // Routing stage // let vf2_post_result = if optimization_level == 0 { let routing_target = PyRoutingTarget::from_target(target)?; - if run_check_map(&dag, target).is_none() { + if !run_check_map(&dag, target).is_none() { let (out_dag, final_layout) = sabre::sabre_routing( &dag, &routing_target, @@ -239,9 +273,20 @@ pub fn transpile( Some(true), )?; dag = out_dag; - let routing_permutation: Vec = final_layout - .iter_virtual() - .map(|(_, x)| Qubit(x.0)) + let routing_permutation: Vec = (0..dag.num_qubits() as u32) + .map(|ref q| { + Qubit( + final_layout + .virtual_to_physical( + transpile_layout + .initial_layout + .as_ref() + .unwrap() + .physical_to_virtual(PhysicalQubit(*q)), + ) + .0, + ) + }) .collect(); transpile_layout.compose_output_permutation(&routing_permutation, true); } @@ -486,7 +531,7 @@ mod tests { }) .collect(); target - .add_instruction(StandardGate::CZ.into(), &[], None, Some(props)) + .add_instruction(StandardGate::ECR.into(), &[], None, Some(props)) .unwrap(); target } @@ -521,11 +566,14 @@ mod tests { Param::Float(0.), ) .unwrap(); - for opt_level in 1..=3 { - let result = transpile(&qc, &target, opt_level, Some(1.0), Some(42)).unwrap(); + for opt_level in 0..=3 { + let result = match transpile(&qc, &target, opt_level, Some(1.0), Some(42)) { + Ok(res) => res, + Err(e) => panic!("Error: {}", e.backtrace()), + }; for inst in result.0.data() { if inst.op.num_qubits() == 2 { - assert_eq!("cz", inst.op.name()); + assert_eq!("ecr", inst.op.name()); target.contains_qargs( &result .0 @@ -634,11 +682,14 @@ mod tests { Param::Float(0.), ) .unwrap(); - for opt_level in 1..=3 { - let result = transpile(&qc, &target, opt_level, Some(1.0), Some(42)).unwrap(); + for opt_level in 0..=3 { + let result = match transpile(&qc, &target, opt_level, Some(1.0), Some(42)) { + Ok(res) => res, + Err(e) => panic!("Error: {}", e.backtrace()), + }; for inst in result.0.data() { if inst.op.num_qubits() == 2 { - assert_eq!("cz", inst.op.name()); + assert_eq!("ecr", inst.op.name()); target.contains_qargs( &result .0 From 53a5d5be3567e1ca2c6c8d5ccccf99a3d05fb2ff Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 26 Aug 2025 12:26:10 -0400 Subject: [PATCH 04/22] Fix clippy --- crates/transpiler/src/transpiler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index 296d366348c9..5af80b97e233 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -262,7 +262,7 @@ pub fn transpile( if optimization_level == 0 { let routing_target = PyRoutingTarget::from_target(target)?; - if !run_check_map(&dag, target).is_none() { + if run_check_map(&dag, target).is_some() { let (out_dag, final_layout) = sabre::sabre_routing( &dag, &routing_target, From 6efbff0903a0e65728fa2448f07be13f0defb898 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 26 Aug 2025 17:23:58 -0400 Subject: [PATCH 05/22] Fix VF2 layout allocation with idle qubits Co-authored-by: Jake Lishman Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> --- .../transpiler/src/passes/vf2/vf2_layout.rs | 42 +++++++++++++++-- test/c/test_transpiler.c | 46 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/crates/transpiler/src/passes/vf2/vf2_layout.rs b/crates/transpiler/src/passes/vf2/vf2_layout.rs index 17de65de5cc6..02175a8bd487 100644 --- a/crates/transpiler/src/passes/vf2/vf2_layout.rs +++ b/crates/transpiler/src/passes/vf2/vf2_layout.rs @@ -325,9 +325,11 @@ fn map_free_qubits( mut partial_layout: HashMap, reverse_im_graph_node_map: &[Option], avg_error_map: &ErrorMap, + dag: &DAGCircuit, target: &Target, ) -> Option> { if free_nodes.is_empty() { + assign_unused_qubits(dag, target, &mut partial_layout); return Some(partial_layout); } let num_physical_qubits = target.num_qubits.unwrap(); @@ -362,9 +364,34 @@ fn map_free_qubits( PhysicalQubit::new(selected_qubit), ); } + assign_unused_qubits(dag, target, &mut partial_layout); Some(partial_layout) } +fn assign_unused_qubits( + dag: &DAGCircuit, + target: &Target, + mapping: &mut HashMap, +) { + if mapping.len() < dag.num_qubits() { + let used_qubits = mapping.values().copied().collect::>(); + let mut unused_qubits = (0..target.num_qubits.unwrap()).filter_map(|x| { + let phys = PhysicalQubit(x); + if !used_qubits.contains(&PhysicalQubit(x)) { + Some(phys) + } else { + None + } + }); + for virt in 0..dag.num_qubits() { + if mapping.contains_key(&VirtualQubit::new(virt as u32)) { + continue; + } + mapping.insert(VirtualQubit(virt as u32), unused_qubits.next().unwrap()); + } + } +} + #[pyfunction] #[pyo3(signature = (dag, target, strict_direction=false, call_limit=None, time_limit=None, max_trials=None, avg_error_map=None))] pub fn vf2_layout_pass( @@ -416,13 +443,15 @@ pub fn vf2_layout_pass( let avg_error_map = avg_error_map.unwrap_or_else(|| build_average_error_map(target)); for mapping in mappings { trials += 1; - let mapping = mapping_to_layout(dag, mapping.unwrap(), &im_graph_data); + let mut mapping = mapping_to_layout(dag, mapping.unwrap(), &im_graph_data); if cm_graph.node_count() == im_graph_data.im_graph.node_count() { + assign_unused_qubits(dag, target, &mut mapping); return Ok(Some(mapping)); } let layout_score = score_layout_internal(&mapping, &avg_error_map, &im_graph_data, strict_direction)?; if layout_score == 0. { + assign_unused_qubits(dag, target, &mut mapping); return Ok(Some(mapping)); } if layout_score < chosen_layout_score { @@ -443,7 +472,10 @@ pub fn vf2_layout_pass( } } } - Ok(chosen_layout) + Ok(chosen_layout.map(|mut layout| { + assign_unused_qubits(dag, target, &mut layout); + layout + })) } else { let cm_graph: Option> = build_coupling_map(target); if cm_graph.is_none() { @@ -461,6 +493,7 @@ pub fn vf2_layout_pass( HashMap::new(), &im_graph_data.reverse_im_graph_node_map, &avg_error_map, + dag, target, )); } @@ -496,13 +529,15 @@ pub fn vf2_layout_pass( let mut chosen_layout_score = f64::MAX; for mapping in mappings { trials += 1; - let mapping = mapping_to_layout(dag, mapping.unwrap(), &im_graph_data); + let mut mapping = mapping_to_layout(dag, mapping.unwrap(), &im_graph_data); if cm_graph.node_count() == im_graph_data.im_graph.node_count() { + assign_unused_qubits(dag, target, &mut mapping); return Ok(Some(mapping)); } let layout_score = score_layout_internal(&mapping, &avg_error_map, &im_graph_data, strict_direction)?; if layout_score == 0. { + assign_unused_qubits(dag, target, &mut mapping); return Ok(Some(mapping)); } if layout_score < chosen_layout_score { @@ -531,6 +566,7 @@ pub fn vf2_layout_pass( chosen_layout, &im_graph_data.reverse_im_graph_node_map, &avg_error_map, + dag, target, )) } diff --git a/test/c/test_transpiler.c b/test/c/test_transpiler.c index 2bdc4b9dc3f1..b798232ed4c5 100644 --- a/test/c/test_transpiler.c +++ b/test/c/test_transpiler.c @@ -152,6 +152,52 @@ int test_transpile_bv(void) { return result; } +int test_transpile_idle_qubits(void) { + int result = Ok; + uint32_t num_qubits = 3; + QkCircuit *circuit = qk_circuit_new(num_qubits, 0); + uint32_t qargs[4]; + double params[1]; + qargs[0] = 2; + qargs[1] = 1; + params[0] = 1.681876; + qk_circuit_gate(circuit, QkGate_CRZ, qargs, params); + QkTarget *target = qk_target_new(num_qubits); + QkTargetEntry *cx_entry = qk_target_entry_new(QkGate_CX); + for (uint32_t i = 0; i < num_qubits - 1; i++) { + qk_target_entry_add_property(cx_entry, (uint32_t[]){i, i + 1}, 2, 0.001 * i, 0.002 * i); + } + qk_target_add_instruction(target, cx_entry); + qk_target_add_instruction(target, qk_target_entry_new(QkGate_U)); + + for (unsigned short opt_level = 0; opt_level < 4; opt_level++) { + QkTranspileOptions transpile_options = {opt_level, 1234, 1.0}; + QkTranspileResult transpile_result; + char *error; + int result_code = + qk_transpile(circuit, target, &transpile_options, &transpile_result, &error); + if (result_code != 0) { + printf("Transpilation failed %s\n", error); + result = EqualityError; + goto cleanup; + } + uint32_t num_instructions = qk_circuit_num_instructions(transpile_result.circuit); + qk_circuit_free(transpile_result.circuit); + qk_transpile_layout_free(transpile_result.layout); + if (num_instructions != 7) { + printf("opt_level: %d num_instructions: %d is not the expected value 7\n", opt_level, + num_instructions); + result = EqualityError; + goto cleanup; + } + } + +cleanup: + qk_circuit_free(circuit); + qk_target_free(target); + return result; +} + int test_transpiler(void) { int num_failed = 0; num_failed += RUN_TEST(test_transpile_bv); From 6e831b6bac8a446ccc57cf1e76688e5141b30735 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 06:38:14 -0400 Subject: [PATCH 06/22] Remove commented out vf2postlayout code Co-authored-by: Jake Lishman --- crates/transpiler/src/transpiler.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index 5af80b97e233..fb9773cbd062 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -258,7 +258,6 @@ pub fn transpile( transpile_layout.compose_output_permutation(&routing_permutation, true); } // Routing stage - // let vf2_post_result = if optimization_level == 0 { let routing_target = PyRoutingTarget::from_target(target)?; @@ -290,29 +289,7 @@ pub fn transpile( .collect(); transpile_layout.compose_output_permutation(&routing_permutation, true); } - // None } - // else if optimization_level == 1 { - // vf2_post_layout_pass(&dag, target, false, Some(50_000), None, Some(2_500), None).unwrap() - // } else if optimization_level == 2 { - // vf2_post_layout_pass(&dag, target, false, Some(50_000), None, Some(2_500), None).unwrap() - // } else { - // vf2_post_layout_pass( - // &dag, - // target, - // false, - // Some(30_000_000), - // None, - // Some(250_000), - // None, - // ) - // .unwrap() - // }; - // if let Some(post_layout) = vf2_post_result { - // update_layout(&mut dag, &mut transpile_layout, |qubit| { - // Qubit(post_layout[VirtualQubit(qubit.0)].0) - // }); - // } // Translation Stage let translation = |dag: &mut DAGCircuit, equiv_lib: &mut EquivalenceLibrary| -> Result<()> { let num_qubits = dag.num_qubits(); From 5c96d44c3533aacb5d89f0a1ca4bfc0b528e5697 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 06:57:37 -0400 Subject: [PATCH 07/22] Run idle qubits test --- test/c/test_transpiler.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/c/test_transpiler.c b/test/c/test_transpiler.c index b798232ed4c5..fae16d65ad4c 100644 --- a/test/c/test_transpiler.c +++ b/test/c/test_transpiler.c @@ -184,7 +184,19 @@ int test_transpile_idle_qubits(void) { uint32_t num_instructions = qk_circuit_num_instructions(transpile_result.circuit); qk_circuit_free(transpile_result.circuit); qk_transpile_layout_free(transpile_result.layout); - if (num_instructions != 7) { + if (opt_level == 0 && num_instructions != 12) { + printf("opt_level: %d num_instructions: %d is not the expected value 12\n", opt_level, + num_instructions); + result = EqualityError; + goto cleanup; + } + if ((opt_level == 1 || opt_level == 3) && num_instructions != 8) { + printf("opt_level: %d num_instructions: %d is not the expected value 8\n", opt_level, + num_instructions); + result = EqualityError; + goto cleanup; + } + if (opt_level == 2 && num_instructions != 7) { printf("opt_level: %d num_instructions: %d is not the expected value 7\n", opt_level, num_instructions); result = EqualityError; @@ -201,6 +213,7 @@ int test_transpile_idle_qubits(void) { int test_transpiler(void) { int num_failed = 0; num_failed += RUN_TEST(test_transpile_bv); + num_failed += RUN_TEST(test_transpile_idle_qubits); fflush(stderr); fprintf(stderr, "=== Number of failed subtests: %i\n", num_failed); From e2426f4340662e72316cdddad14cd76d6870f579 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 07:22:26 -0400 Subject: [PATCH 08/22] Add release note --- releasenotes/notes/transpile-c-3d8005b1d67705c9.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/transpile-c-3d8005b1d67705c9.yaml diff --git a/releasenotes/notes/transpile-c-3d8005b1d67705c9.yaml b/releasenotes/notes/transpile-c-3d8005b1d67705c9.yaml new file mode 100644 index 000000000000..92ed3162c7aa --- /dev/null +++ b/releasenotes/notes/transpile-c-3d8005b1d67705c9.yaml @@ -0,0 +1,10 @@ +--- +features_c: + - | + Added a new function ``transpile()`` to the Qiskit C API. This function is used for transpiling + quantum circuits in a standalone C context without using Python. This is the last major component + needed in the C API for typical hardware execution workflows using Qiskit. + + This function mirrors the preset pass managers that are used for the Python transpiler except for + some passes and functionality is skipped if it is not relevant for circuits constructed using the C + API. This makes the function only suitable for standalone C contexts. From 3bbd9d39f0c82eb9b96ce5e03cc784f0d59307ee Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 07:38:10 -0400 Subject: [PATCH 09/22] Move MinPointState definition outside transpile func and simplify interface --- crates/transpiler/src/transpiler.rs | 82 +++++++++++++++-------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index fb9773cbd062..e6b8b2deb2e5 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -372,44 +372,9 @@ pub fn transpile( new_size = Some(dag.size(false)?); } } else if optimization_level == 3 { - struct MinPointState { - best_depth: Option, - best_size: Option, - count: usize, - best_dag: DAGCircuit, - } - let mut min_state = MinPointState { - best_depth: None, - best_size: None, - count: 0, - best_dag: dag.clone(), - }; - impl MinPointState { - fn check( - &mut self, - dag: &DAGCircuit, - new_size: Option, - new_depth: Option, - ) -> bool { - if self.best_depth.is_none() || self.best_size.is_none() { - self.best_depth = new_depth; - self.best_size = new_size; - self.best_dag = dag.clone(); - true - } else if (new_depth, new_size) > (self.best_depth, self.best_size) { - self.count += 1; - true - } else if (new_depth, new_size) < (self.best_depth, self.best_size) { - self.count = 1; - self.best_depth = new_depth; - self.best_size = new_size; - true - } else { - (new_depth, new_size) != (self.best_depth, self.best_size) - } - } - } let mut continue_loop: bool = true; + let mut min_state = MinPointState::new(&dag); + while continue_loop { run_consolidate_blocks(&mut dag, false, approximation_degree, Some(target))?; let num_qubits = dag.num_qubits(); @@ -432,15 +397,52 @@ pub fn transpile( if gates_missing_from_target(&dag, target)? { translation(&mut dag, &mut equivalence_library)?; } - new_depth = Some(dag.depth(false)?); - new_size = Some(dag.size(false)?); - continue_loop = min_state.check(&dag, new_size, new_depth); + continue_loop = min_state.update_with(&dag); } dag = min_state.best_dag; } Ok((dag_to_circuit(&dag, false)?, transpile_layout)) } +struct MinPointState { + best_depth: Option, + best_size: Option, + count: usize, + best_dag: DAGCircuit, +} + +impl MinPointState { + fn new(dag: &DAGCircuit) -> Self { + MinPointState { + best_depth: None, + best_size: None, + count: 0, + best_dag: dag.clone(), + } + } + + fn update_with(&mut self, dag: &DAGCircuit) -> bool { + let new_depth = Some(dag.depth(false).unwrap()); + let new_size = Some(dag.size(false).unwrap()); + if self.best_depth.is_none() || self.best_size.is_none() { + self.best_depth = new_depth; + self.best_size = new_size; + self.best_dag = dag.clone(); + true + } else if (new_depth, new_size) > (self.best_depth, self.best_size) { + self.count += 1; + true + } else if (new_depth, new_size) < (self.best_depth, self.best_size) { + self.count = 1; + self.best_depth = new_depth; + self.best_size = new_size; + true + } else { + (new_depth, new_size) != (self.best_depth, self.best_size) + } + } +} + #[cfg(all(test, not(miri)))] mod tests { use super::*; From 1a89102c814ab36e462d8a8d8df70099e8bdcf4c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:17:53 -0400 Subject: [PATCH 10/22] Apply suggestions from code review Co-authored-by: Julien Gacon Co-authored-by: Jake Lishman --- crates/cext/src/transpiler/transpile_function.rs | 7 ++++--- docs/cdoc/qk-transpiler.rst | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index de077e9bea00..50665f35e2ee 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -37,7 +37,7 @@ pub struct TranspileOptions { /// The optimization level to run the transpiler with. Valid values are 0, 1, 2, or 3. optimization_level: u8, /// The seed for the transpiler. If set to a negative number this means no seed will be - /// set and the RNGs used in the transpiler will be seeded froms system entropy. + /// set and the RNGs used in the transpiler will be seeded from system entropy. seed: i64, /// The approximation degree a heurstic dial where 1.0 means no approximation (up to numerical /// tolerance) and 0.0 means the maximum approximation. A `NAN` value indicates that @@ -82,8 +82,9 @@ pub extern "C" fn qk_transpiler_default_options() -> TranspileOptions { /// /// @param circuit A pointer to the circuit to run the transpiler on /// @param target A pointer to the target to compile the circuit for -/// @params options A pointer to an options object that define user options if this is a null -/// pointer the default values will be used +/// @params options A pointer to an options object that defines user options. If this is a null +/// pointer the default values will be used. See ``qk_transpile_default_options`` +/// for more details on the default values. /// @param result A pointer to the memory location of the transpiler result. On a successful /// execution (return code 0) the output of the transpiler will be written to the pointer /// @param error A pointer to a pointer with an nul terminated string with an error description. diff --git a/docs/cdoc/qk-transpiler.rst b/docs/cdoc/qk-transpiler.rst index 3956fcd19805..04bb58a80e05 100644 --- a/docs/cdoc/qk-transpiler.rst +++ b/docs/cdoc/qk-transpiler.rst @@ -2,10 +2,10 @@ QkTranspiler ============ -The ``qk_transpiler()`` function exposes the :mod:`qiskit.transpiler` to C. -The basic functionality is using the same underlying but the transpiler as +The :c:func:`qk_transpiler` function exposes Qiskit's tranpsiler (:py:mod:`qiskit.transpiler`) to C. +The basic functionality is using the same underlying code as the Python-space version, but the transpiler as exposed to C has more limitations than what is exposed to Python. The transpiler -assumes a circuit built constructed using solely the C API and is intented to +assumes a circuit built constructed using solely the C API and is intended to work solely in the case of a standalone C API. It will potentially not work correctly when in a mixed Python/C use case. If you're mixing C and Python you should call the :py:func:`.generate_preset_pass_manager` or From e5cbe55511ff1f669401b482411b22d33706df5f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:15:57 -0400 Subject: [PATCH 11/22] Docs updates --- .../cext/src/transpiler/transpile_function.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index 50665f35e2ee..c370658cc56b 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -24,7 +24,12 @@ use crate::pointers::const_ptr_as_ref; /// The container result object from ``qk_transpile`` /// /// When the transpiler successfully compiles a quantum circuit for a given target it -/// returns the transpiled circuit and the layout. +/// returns the transpiled circuit and the layout. The ``qk_transpile`` function will +/// write pointers to the fields in this struct when it successfully executes, you can +/// initialize this struct with null pointers or leave them unset as the values are never +/// read by ``qk_transpile`` and only written to. After calling ``qk_transpile`` you are +/// responsible for calling ``qk_circuit_free`` and ``qk_transpile_layout_free`` on the +/// members of this struct. #[repr(C)] pub struct TranspileResult { circuit: *mut CircuitData, @@ -68,17 +73,20 @@ pub extern "C" fn qk_transpiler_default_options() -> TranspileOptions { } /// @ingroup QkTranspiler -/// Transpile a single circuit that was constructed using the C API +/// Transpile a single circuit /// /// The Qiskit transpiler is a quantum circuit compiler that rewrites a given /// input circuit to match the constraints of a QPU and/or optimize the circuit -/// for execution. +/// for execution. This function should only be used with circuit's constructed solely +/// using Qiskit's C API, it makes assumptions on the circuit only using features exposed via C, +/// if you are in a mixed Python and C environment it is typically better to invoke the transpiler +/// via Python. /// /// This function is multithreaded internally and will launch a thread pool -/// with threads equal to the number of CPUs by default. You can tune the -/// number of threads with the ``RAYON_NUM_THREADS`` environment variable. -/// For example, setting ``RAYON_NUM_THREADS=4`` would limit the thread pool -/// to 4 threads. +/// with threads equal to the number of CPUs reported by the operating system by default. +/// This will include logical cores on CPUs with SMT. You can tune the number of threads +/// with the ``RAYON_NUM_THREADS`` environment variable. For example, setting +/// ``RAYON_NUM_THREADS=4`` would limit the thread pool to 4 threads. /// /// @param circuit A pointer to the circuit to run the transpiler on /// @param target A pointer to the target to compile the circuit for @@ -86,7 +94,9 @@ pub extern "C" fn qk_transpiler_default_options() -> TranspileOptions { /// pointer the default values will be used. See ``qk_transpile_default_options`` /// for more details on the default values. /// @param result A pointer to the memory location of the transpiler result. On a successful -/// execution (return code 0) the output of the transpiler will be written to the pointer +/// execution (return code 0) the output of the transpiler will be written to the pointer. The +/// members of the result struct are owned by the caller and you are responsible for freeing +/// the members using the respective free functions. /// @param error A pointer to a pointer with an nul terminated string with an error description. /// If the transpiler fails a pointer to the string with the error description will be written /// to this pointer. That pointer needs to be freed with `qk_str_free`. From 3a29e4c0a151a3934678ca775d9df67b3848d92e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:38:43 -0400 Subject: [PATCH 12/22] Allow error to be null and just don't write out error msg --- .../cext/src/transpiler/transpile_function.rs | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index c370658cc56b..470de82abbdd 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -99,16 +99,18 @@ pub extern "C" fn qk_transpiler_default_options() -> TranspileOptions { /// the members using the respective free functions. /// @param error A pointer to a pointer with an nul terminated string with an error description. /// If the transpiler fails a pointer to the string with the error description will be written -/// to this pointer. That pointer needs to be freed with `qk_str_free`. +/// to this pointer. That pointer needs to be freed with `qk_str_free`. This can be a null +/// pointer in which case the error will not be written out. /// /// @returns the return code for the transpiler, 0 means success and all other values indicate an /// error /// /// # Safety /// -/// Behavior is undefined if ``circuit``, ``target``, ``options``, ``result``, or ``error`` are -/// not valid, non-null pointers to a ``QkCircuit``, ``QkTarget``, ``QkTranspileOptions``, -/// ``QkTranspileResult``, or a ``char`` pointer respectively. +/// Behavior is undefined if ``circuit``, ``target``, ``options``, or ``result``, are +/// not valid, non-null pointers to a ``QkCircuit``, ``QkTarget``, ``QkTranspileOptions``, or +/// ``QkTranspileResult`` respectively. ``error`` must be a valid pointer to a ``char`` pointer +/// or ``NULL``. #[no_mangle] #[cfg(feature = "cbinding")] pub unsafe extern "C" fn qk_transpile( @@ -144,14 +146,16 @@ pub unsafe extern "C" fn qk_transpile( if let Some(target_qubits) = target.num_qubits { if target_qubits < qc.num_qubits() as u32 { - unsafe { - *error = CString::new(format!( - "Insufficient qubits in target: {}, the circuit uses {}", - target_qubits, - qc.num_qubits() - )) - .unwrap() - .into_raw(); + if !error.is_null() { + unsafe { + *error = CString::new(format!( + "Insufficient qubits in target: {}, the circuit uses {}", + target_qubits, + qc.num_qubits() + )) + .unwrap() + .into_raw(); + } } return 1; } @@ -174,17 +178,19 @@ pub unsafe extern "C" fn qk_transpile( 0 } Err(e) => { - unsafe { - // Right now we return a backtrace of the error. This at least gives a hint as to - // which pass failed when we have rust errors normalized we can actually have error - // messages which are user facing. But most likely this will be a PyErr and panic - // when trying to extract the string. - *error = CString::new(format!( - "Transpilation failed with this backtrace: {}", - e.backtrace() - )) - .unwrap() - .into_raw(); + if !error.is_null() { + unsafe { + // Right now we return a backtrace of the error. This at least gives a hint as to + // which pass failed when we have rust errors normalized we can actually have error + // messages which are user facing. But most likely this will be a PyErr and panic + // when trying to extract the string. + *error = CString::new(format!( + "Transpilation failed with this backtrace: {}", + e.backtrace() + )) + .unwrap() + .into_raw(); + } } 1 } From 85597f32518433aacea577dd8809fe7fea4e262c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:50:26 -0400 Subject: [PATCH 13/22] Use an enum for optimization level --- .../cext/src/transpiler/transpile_function.rs | 5 ++- crates/transpiler/src/transpiler.rs | 44 +++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index 470de82abbdd..384cdf16b777 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -124,12 +124,13 @@ pub unsafe extern "C" fn qk_transpile( let qc = unsafe { const_ptr_as_ref(qc) }; let target = unsafe { const_ptr_as_ref(target) }; let options = unsafe { const_ptr_as_ref(options) }; - if ![0, 1, 2, 3].contains(&options.optimization_level) { + if !(0..=3u8).contains(&options.optimization_level) { panic!( "Invalid optimization level specified {}", options.optimization_level ); } + let seed = if options.seed < 0 { None } else { @@ -164,7 +165,7 @@ pub unsafe extern "C" fn qk_transpile( match transpile( qc, target, - options.optimization_level, + options.optimization_level.into(), approximation_degree, seed, ) { diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index e6b8b2deb2e5..4b9fc96ec153 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -26,6 +26,27 @@ use qiskit_circuit::converters::dag_to_circuit; use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::{PhysicalQubit, Qubit}; +#[derive(Copy, Eq, PartialEq, Debug, Clone)] +#[repr(u8)] +pub enum OptimizationLevel { + Level0 = 0, + Level1 = 1, + Level2 = 2, + Level3 = 3, +} + +impl From for OptimizationLevel { + fn from(value: u8) -> Self { + match value { + 0 => Self::Level0, + 1 => Self::Level1, + 2 => Self::Level2, + 3 => Self::Level3, + _ => panic!("Invalid optimization level specified {value}"), + } + } +} + /// A transpilation function for Rust native circuits for use in the C API. This will not cover /// things that only exist in the Python API such as custom gates or control flow. When those /// concepts exist in the rust data model this function must be expanded before adding them to the @@ -33,13 +54,10 @@ use qiskit_circuit::{PhysicalQubit, Qubit}; pub fn transpile( circuit: &CircuitData, target: &Target, - optimization_level: u8, + optimization_level: OptimizationLevel, approximation_degree: Option, seed: Option, ) -> Result<(CircuitData, TranspileLayout)> { - if !(0..=3u8).contains(&optimization_level) { - panic!("Invalid optimization level specified {optimization_level}"); - } let mut dag = DAGCircuit::from_circuit_data(circuit, false, None, None, None, None)?; let mut commutation_checker = get_standard_commutation_checker(); let mut equivalence_library = generate_standard_equivalence_library(); @@ -74,9 +92,9 @@ pub fn transpile( // Init stage unroll_3q_or_more(&mut dag)?; - if optimization_level == 1 { + if optimization_level == OptimizationLevel::Level1 { run_inverse_cancellation_standard_gates(&mut dag); - } else if optimization_level == 2 || optimization_level == 3 { + } else if matches!(optimization_level, OptimizationLevel::Level2 | OptimizationLevel::Level3) { if let Some((new_dag, permutation)) = run_elide_permutations(&dag)? { dag = new_dag; let permutation: Vec = permutation.into_iter().map(Qubit::new).collect(); @@ -113,7 +131,7 @@ pub fn transpile( .with_lookahead(0.5, 20, sabre::SetScaling::Size) .with_decay(0.001, 5)?; - if optimization_level == 0 { + if optimization_level == OptimizationLevel::Level0 { // Apply a trivial layout apply_layout( &mut dag, @@ -121,7 +139,7 @@ pub fn transpile( target.num_qubits.unwrap(), |x| PhysicalQubit(x.0), ); - } else if optimization_level == 1 { + } else if optimization_level == OptimizationLevel::Level1 { if run_check_map(&dag, target).is_none() { apply_layout( &mut dag, @@ -169,7 +187,7 @@ pub fn transpile( .collect(); transpile_layout.compose_output_permutation(&routing_permutation, true); } - } else if optimization_level == 2 { + } else if optimization_level == OptimizationLevel::Level2 { if let Some(vf2_result) = vf2_layout_pass(&dag, target, false, Some(5_000_000), None, Some(2500), None)? { @@ -258,7 +276,7 @@ pub fn transpile( transpile_layout.compose_output_permutation(&routing_permutation, true); } // Routing stage - if optimization_level == 0 { + if optimization_level == OptimizationLevel::Level0 { let routing_target = PyRoutingTarget::from_target(target)?; if run_check_map(&dag, target).is_some() { @@ -327,7 +345,7 @@ pub fn transpile( let mut size: Option = None; let mut new_depth; let mut new_size; - if optimization_level == 1 { + if optimization_level == OptimizationLevel::Level1 { new_depth = Some(dag.depth(false)?); new_size = Some(dag.size(false)?); while new_depth != depth || new_size != size { @@ -341,7 +359,7 @@ pub fn transpile( new_depth = Some(dag.depth(false)?); new_size = Some(dag.size(false)?); } - } else if optimization_level == 2 { + } else if optimization_level == OptimizationLevel::Level2 { run_consolidate_blocks(&mut dag, false, approximation_degree, Some(target))?; let num_qubits = dag.num_qubits(); dag = run_unitary_synthesis( @@ -371,7 +389,7 @@ pub fn transpile( new_depth = Some(dag.depth(false)?); new_size = Some(dag.size(false)?); } - } else if optimization_level == 3 { + } else if optimization_level == OptimizationLevel::Level3 { let mut continue_loop: bool = true; let mut min_state = MinPointState::new(&dag); From 16b6a98645109350a7c0267da195bbb2bb06d0ba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:54:30 -0400 Subject: [PATCH 14/22] Fix c test issues --- crates/cext/src/transpiler/transpile_function.rs | 2 +- crates/transpiler/src/transpiler.rs | 5 ++++- test/c/test_transpiler.c | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index 384cdf16b777..16451b390d45 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -186,7 +186,7 @@ pub unsafe extern "C" fn qk_transpile( // messages which are user facing. But most likely this will be a PyErr and panic // when trying to extract the string. *error = CString::new(format!( - "Transpilation failed with this backtrace: {}", + "Transpilation failed with this backtrace: {}", e.backtrace() )) .unwrap() diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index 4b9fc96ec153..42bdc1474abb 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -94,7 +94,10 @@ pub fn transpile( unroll_3q_or_more(&mut dag)?; if optimization_level == OptimizationLevel::Level1 { run_inverse_cancellation_standard_gates(&mut dag); - } else if matches!(optimization_level, OptimizationLevel::Level2 | OptimizationLevel::Level3) { + } else if matches!( + optimization_level, + OptimizationLevel::Level2 | OptimizationLevel::Level3 + ) { if let Some((new_dag, permutation)) = run_elide_permutations(&dag)? { dag = new_dag; let permutation: Vec = permutation.into_iter().map(Qubit::new).collect(); diff --git a/test/c/test_transpiler.c b/test/c/test_transpiler.c index fae16d65ad4c..e67292aa3676 100644 --- a/test/c/test_transpiler.c +++ b/test/c/test_transpiler.c @@ -80,7 +80,7 @@ int test_transpile_bv(void) { qk_circuit_gate(qc, QkGate_H, qargs, NULL); } for (uint32_t i = 0; i < qk_circuit_num_qubits(qc) - 1; i += 2) { - uint32_t qargs[2] = {i, 9}; + uint32_t qargs[2] = {i, num_qubits - 1}; qk_circuit_gate(qc, QkGate_CX, qargs, NULL); } QkTranspileResult transpile_result = {NULL, NULL}; @@ -91,6 +91,7 @@ int test_transpile_bv(void) { if (result_code != 0) { printf("Transpilation failed with: %s\n", error); result = EqualityError; + qk_str_free(error); goto circuit_cleanup; } @@ -179,6 +180,7 @@ int test_transpile_idle_qubits(void) { if (result_code != 0) { printf("Transpilation failed %s\n", error); result = EqualityError; + qk_str_free(error); goto cleanup; } uint32_t num_instructions = qk_circuit_num_instructions(transpile_result.circuit); From 7e6c5bac0025157668990054ebc5917353522275 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 09:58:36 -0400 Subject: [PATCH 15/22] Use ExitCode enum for C return type --- crates/cext/src/exit_codes.rs | 2 ++ crates/cext/src/transpiler/transpile_function.rs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/cext/src/exit_codes.rs b/crates/cext/src/exit_codes.rs index f1e15a224881..07f81c71618d 100644 --- a/crates/cext/src/exit_codes.rs +++ b/crates/cext/src/exit_codes.rs @@ -54,6 +54,8 @@ pub enum ExitCode { TargetInvalidQargsKey = 303, /// Querying an operation that doesn't exist in the Target. TargetInvalidInstKey = 304, + /// Transpilation failed + TranspilerError = 400, } impl From for ExitCode { diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index 16451b390d45..9097f809bfb0 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -18,6 +18,7 @@ use qiskit_transpiler::target::Target; use qiskit_transpiler::transpile; use qiskit_transpiler::transpile_layout::TranspileLayout; +use crate::exit_codes::ExitCode; use crate::pointers::const_ptr_as_ref; /// @ingroup QkTranspiler @@ -119,7 +120,7 @@ pub unsafe extern "C" fn qk_transpile( options: *const TranspileOptions, result: *mut TranspileResult, error: *mut *mut c_char, -) -> i32 { +) -> ExitCode { // SAFETY: Per documentation, the pointer is non-null and aligned. let qc = unsafe { const_ptr_as_ref(qc) }; let target = unsafe { const_ptr_as_ref(target) }; @@ -158,7 +159,7 @@ pub unsafe extern "C" fn qk_transpile( .into_raw(); } } - return 1; + return ExitCode::TranspilerError; } } @@ -176,7 +177,7 @@ pub unsafe extern "C" fn qk_transpile( layout: Box::into_raw(Box::new(transpile_result.1)), }; } - 0 + ExitCode::Success } Err(e) => { if !error.is_null() { @@ -193,7 +194,7 @@ pub unsafe extern "C" fn qk_transpile( .into_raw(); } } - 1 + ExitCode::TranspilerError } } } From 35915b6860608dfabb41e5eba22d4d401824b843 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 10:02:19 -0400 Subject: [PATCH 16/22] Add comment about run_check_map return --- crates/transpiler/src/transpiler.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index 42bdc1474abb..e9d4813adc6e 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -143,6 +143,9 @@ pub fn transpile( |x| PhysicalQubit(x.0), ); } else if optimization_level == OptimizationLevel::Level1 { + // run_check_map returns Some((gate, qargs)) if the circuit violates the connectivity + // constraints in the target and returns None if the circuit conforms to the undirected + // connectivity constraints if run_check_map(&dag, target).is_none() { apply_layout( &mut dag, @@ -281,7 +284,9 @@ pub fn transpile( // Routing stage if optimization_level == OptimizationLevel::Level0 { let routing_target = PyRoutingTarget::from_target(target)?; - + // run_check_map returns Some((gate, qargs)) if the circuit violates the connectivity + // constraints in the target and returns None if the circuit conforms to the undirected + // connectivity constraints if run_check_map(&dag, target).is_some() { let (out_dag, final_layout) = sabre::sabre_routing( &dag, From 9986cb9472684b57968c258038d0f3521d14f491 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 27 Aug 2025 10:12:47 -0400 Subject: [PATCH 17/22] Fix rust tests --- crates/transpiler/src/transpiler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index e9d4813adc6e..c41c23c0fd04 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -572,7 +572,7 @@ mod tests { ) .unwrap(); for opt_level in 0..=3 { - let result = match transpile(&qc, &target, opt_level, Some(1.0), Some(42)) { + let result = match transpile(&qc, &target, opt_level.into(), Some(1.0), Some(42)) { Ok(res) => res, Err(e) => panic!("Error: {}", e.backtrace()), }; @@ -688,7 +688,7 @@ mod tests { ) .unwrap(); for opt_level in 0..=3 { - let result = match transpile(&qc, &target, opt_level, Some(1.0), Some(42)) { + let result = match transpile(&qc, &target, opt_level.into(), Some(1.0), Some(42)) { Ok(res) => res, Err(e) => panic!("Error: {}", e.backtrace()), }; From bea12c46e3132485269098e142f5bbfc68a63b3e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 27 Aug 2025 14:22:44 +0100 Subject: [PATCH 18/22] Add permutation-mutation methods to `TranspileLayout` This rewrites how permutations are mutated within the `TranspileLayout` structure, avoiding exposing the internal state (which is desperately easy to update incorrectly when done piecewise) and instead exposing methods that let us either add in a new permutation between the circuit and the existing tracked layout/permutation (the typical thing we're doing), or to include (only) a permutation that was being tracked before this new `TranspileLayout` object was created. The `add_permutation_outside` method may well be obsoleted in the future once Sabre is properly acting on `TranspileLayout` itself. --- .../cext/src/transpiler/transpile_layout.rs | 2 +- crates/transpiler/src/passes/apply_layout.rs | 2 +- crates/transpiler/src/transpile_layout.rs | 205 ++++++++++++++---- crates/transpiler/src/transpiler.rs | 111 ++++------ 4 files changed, 197 insertions(+), 123 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_layout.rs b/crates/cext/src/transpiler/transpile_layout.rs index a0694d4e44ef..1ba934053caf 100644 --- a/crates/cext/src/transpiler/transpile_layout.rs +++ b/crates/cext/src/transpiler/transpile_layout.rs @@ -95,7 +95,7 @@ pub unsafe extern "C" fn qk_transpile_layout_initial_layout( ) -> bool { // SAFETY: Per the documentation layout must be a valid pointer to a TranspileLayout let layout = unsafe { const_ptr_as_ref(layout) }; - let out_initial_layout = layout.initial_layout(filter_ancillas); + let out_initial_layout = layout.initial_physical_layout(filter_ancillas); if let Some(out_initial_layout) = out_initial_layout { // SAFETY: Per the documentation initial_layout must be a valid pointer with a sufficient // allocation for the output array diff --git a/crates/transpiler/src/passes/apply_layout.rs b/crates/transpiler/src/passes/apply_layout.rs index 7328f65d5160..310d1d4bfdeb 100644 --- a/crates/transpiler/src/passes/apply_layout.rs +++ b/crates/transpiler/src/passes/apply_layout.rs @@ -41,7 +41,7 @@ pub fn apply_layout( num_physical_qubits: u32, mut layout_fn: impl FnMut(VirtualQubit) -> PhysicalQubit, ) { - if cur_layout.initial_layout(false).is_some() { + if cur_layout.initial_physical_layout(false).is_some() { panic!("cannot apply a layout when one is already set"); } diff --git a/crates/transpiler/src/transpile_layout.rs b/crates/transpiler/src/transpile_layout.rs index b34f4dab2ee3..a33a86106402 100644 --- a/crates/transpiler/src/transpile_layout.rs +++ b/crates/transpiler/src/transpile_layout.rs @@ -35,12 +35,12 @@ use qiskit_circuit::imports::TRANSPILE_LAYOUT; pub struct TranspileLayout { /// The initial layout which is mapping the virtual qubits in the input circuit to the /// transpiler to the physical qubits used on the transpilation target - pub initial_layout: Option, + initial_layout: Option, /// The optional routing permutation that /// represents the permutation caused by routing or permutation elision during /// transpilation. This vector maps the qubits at the start of the circuit to their /// final position/physical qubit at the end of the circuit. - pub output_permutation: Option>, + output_permutation: Option>, /// The virtual qubits [`ShareableQubit`] objects from the input circuit to the transpiler. /// This vec should be arranged in order as in the original circuit, the index of the `Vec` /// corresponds to the [`VirtualQubit`] in the `initial_layout` attribute. This should include @@ -84,6 +84,62 @@ impl TranspileLayout { } } + /// Create a [TranspileLayout] object from a pair of layouts, rather than the natural + /// constituent parts. + /// + /// # Panics + /// + /// If the two layouts and the number of virtual qubits are not all the same size, or if the + /// numebr of input qubits is larger than the sizes of the other objects. + pub fn from_layouts( + initial_layout: NLayout, + final_layout: &NLayout, + virtual_qubits: Vec, + num_input_qubits: u32, + ) -> Self { + assert!( + initial_layout.num_qubits() == virtual_qubits.len(), + "number of virtual qubit objects did not match the given layouts" + ); + assert!( + num_input_qubits as usize <= virtual_qubits.len(), + "cannot have more input qubits than ancilla-expanded virtual qubits" + ); + Self { + output_permutation: Some(Self::permutation_from_layouts( + &initial_layout, + final_layout, + )), + initial_layout: Some(initial_layout), + virtual_qubits, + num_input_qubits, + } + } + + /// Create a permutation vector in the same convention as [TranspileLayout] that represents the + /// same permutation as between two explicit layouts. + pub fn permutation_from_layouts( + initial_layout: &NLayout, + final_layout: &NLayout, + ) -> Vec { + assert!( + initial_layout.num_qubits() == final_layout.num_qubits(), + "layout objects had mismatched numbers of qubits" + ); + (0..initial_layout.num_qubits()) + .map(|i| { + PhysicalQubit::new(i as u32) + .to_virt(initial_layout) + .to_phys(final_layout) + .into() + }) + .collect() + } + + pub fn initial_layout(&self) -> Option<&NLayout> { + self.initial_layout.as_ref() + } + /// The number of input circuit qubits pub fn num_input_qubits(&self) -> u32 { self.num_input_qubits @@ -106,7 +162,7 @@ impl TranspileLayout { /// `filter_ancillas` - If set to `true` any ancilla qubits added to /// the circuit by the transpiler will not be included in the output /// array. - pub fn initial_layout(&self, filter_ancillas: bool) -> Option> { + pub fn initial_physical_layout(&self, filter_ancillas: bool) -> Option> { self.initial_layout.as_ref().map(|layout| { if filter_ancillas { (0..self.num_input_qubits()) @@ -274,36 +330,6 @@ impl TranspileLayout { NLayout::from_virtual_to_physical(self.final_index_layout(false)).unwrap() } - /// Compose another routing permutation into the contained in this layout - /// - /// # Args - /// - /// * `other` - The other permutation array to compose with - /// * `reverse` - Whether to compose in reverse order - pub fn compose_output_permutation(&mut self, other: &[Qubit], reverse: bool) { - if let Some(ref output_permutation) = self.output_permutation { - let new_perm = if !reverse { - let mut new_perm = output_permutation.clone(); - output_permutation - .iter() - .enumerate() - .for_each(|(idx, qubit)| { - new_perm[idx] = other[qubit.index()]; - }); - new_perm - } else { - let mut new_perm = other.to_vec(); - other.iter().enumerate().for_each(|(idx, qubit)| { - new_perm[idx] = output_permutation[qubit.index()]; - }); - new_perm - }; - self.output_permutation = Some(new_perm); - } else { - self.output_permutation = Some(other.to_vec()); - } - } - /// Update the initial layout by permuting the output-space qubit indices from the input of the /// `relabel_fn` to its output. /// @@ -353,6 +379,90 @@ impl TranspileLayout { self.output_permutation = output_permutation; } + /// Add an extra "undoing" permutation that comes inbetween the [DAGCircuit] and any permutation + /// already stored in this layout. + /// + /// This is typically what transpiler passes should call if they are mutating a + /// [TranspileLayout] they received, to add new information; if you mutate a [DAGCircuit] + /// inducing an additional permutation to track, then you call this method with the new + /// permutation to add it to the existing layout tracking. + /// + /// If the combination of this layout and the DAG originally formed a triple like: + /// ```text + /// (initial layout, DAG, output permutation) + /// ``` + /// and a transpiler passes splits the DAG into + /// ```text + /// (DAG, new permutation) + /// ``` + /// it would then call this function with the new permutation in order to combine it into the + /// existing layout (whether or not the initial layout and prior output permutation were set). + /// + /// The function is a "gets set to" permutation, just like as returned by [output_permutation]. + /// + /// The `permutation` function will be called once for each qubit index in the "output" space + /// (in other words, the range from 0 up to but excluding [num_output_qubits]). + pub fn add_permutation_inside(&mut self, mut permutation: impl FnMut(Qubit) -> Qubit) { + let new_permutation = match self.output_permutation.as_ref() { + Some(previous) => previous.iter().map(|q| permutation(*q)).collect(), + None => (0..self.num_output_qubits()) + .map(|q| permutation(Qubit(q))) + .collect(), + }; + self.output_permutation = Some(new_permutation); + } + + /// Add an extra "undoing" permutation that applied to the virtual qubits of the input circuit + /// before the the rest of this layout was split out from the DAG. + /// + /// Say a pass like `ElidePermutations` had run, and so the current state of the transpiler IR + /// was + /// ```text + /// (virtual DAG, virtual permutation) + /// ``` + /// Then, a layout and/or routing pass run on "virtual DAG", ignoring the virtual permutation, + /// so we now have a situation where we hold + /// ```text + /// ((initial layout, physical DAG, routing permutation), virtual permutation) + /// ``` + /// and this [TranspileLayout] corresponds to the inner 3-tuple. In order to combine the + /// previous virtual permutation into this layout, you wuold call this function. + /// + /// The function is a "gets set to" permutation, just like as returned by [output_permutation]. + /// + /// The `permutation` function will be called once for each qubit index in the "input" space + /// (in other words, the range from 0 up to but excluding [num_input_qubits]). + /// + /// # Panics + /// + /// If this layout does not have a set [initial_layout]. + pub fn add_permutation_outside( + &mut self, + mut permutation: impl FnMut(VirtualQubit) -> VirtualQubit, + ) { + let initial = self + .initial_layout + .as_ref() + .expect("it only makes sense to call this function when the layout is set"); + // These are logically physical qubits, but we want to be able to assign it directly. + let mut physical_permutation = vec![Qubit::MAX; self.num_output_qubits() as usize]; + for virt in (0..self.num_input_qubits()).map(VirtualQubit) { + let phys = virt.to_phys(initial); + physical_permutation[phys.index()] = permutation(virt).to_phys(initial).into(); + } + for ancilla in (self.num_input_qubits()..self.num_output_qubits()).map(VirtualQubit) { + physical_permutation[ancilla.to_phys(initial).index()] = + ancilla.to_phys(initial).into(); + } + let new_permutation = match self.output_permutation.as_ref() { + Some(current) => (0..self.num_output_qubits()) + .map(|q| current[physical_permutation[q as usize].index()]) + .collect(), + None => physical_permutation, + }; + self.output_permutation = Some(new_permutation); + } + // TODO: Conditionally compile this method so we don't depend on symbols from Python /// Return a Python space `TranspileLayout` object built from this rust space `TranspileLayout` /// @@ -512,7 +622,7 @@ mod test_transpile_layout { initial_qubits, 3, ); - let result = layout.initial_layout(false); + let result = layout.initial_physical_layout(false); assert_eq!( Some(vec![PhysicalQubit(2), PhysicalQubit(1), PhysicalQubit(0)]), result @@ -629,7 +739,7 @@ mod test_transpile_layout { initial_qubits, 3, ); - let result = layout.initial_layout(true); + let result = layout.initial_physical_layout(true); assert_eq!( Some(vec![PhysicalQubit(9), PhysicalQubit(4), PhysicalQubit(0)]), result @@ -851,7 +961,7 @@ mod test_transpile_layout { initial_qubits, 3, ); - let result = layout.initial_layout(false); + let result = layout.initial_physical_layout(false); assert_eq!(Some(expected), result) } @@ -884,7 +994,7 @@ mod test_transpile_layout { let initial_layout = NLayout::from_virtual_to_physical(initial_layout_vec).unwrap(); let initial_qubits = vec![ShareableQubit::new_anonymous(); 3]; let layout = TranspileLayout::new(Some(initial_layout), None, initial_qubits, 3); - let result = layout.initial_layout(false); + let result = layout.initial_physical_layout(false); assert_eq!(Some(expected), result); } @@ -940,7 +1050,7 @@ mod test_transpile_layout { let initial_layout = NLayout::from_virtual_to_physical(initial_layout_vec).unwrap(); let initial_qubits = vec![ShareableQubit::new_anonymous(); 5]; let layout = TranspileLayout::new(Some(initial_layout), None, initial_qubits, 3); - let result = layout.initial_layout(true); + let result = layout.initial_physical_layout(true); assert_eq!( Some(vec![PhysicalQubit(2), PhysicalQubit(4), PhysicalQubit(0)]), result @@ -1027,25 +1137,26 @@ mod test_transpile_layout { let initial_layout = NLayout::from_virtual_to_physical(initial_layout_vec).unwrap(); let initial_qubits = vec![ShareableQubit::new_anonymous(); 5]; let layout = TranspileLayout::new(Some(initial_layout), None, initial_qubits, 3); - let result = layout.initial_layout(false); + let result = layout.initial_physical_layout(false); assert_eq!(Some(expected), result); } #[test] fn test_compose() { let first = vec![Qubit(0), Qubit(3), Qubit(1), Qubit(2)]; - let second = vec![Qubit(2), Qubit(3), Qubit(1), Qubit(0)]; + let second = [2, 3, 1, 0].map(Qubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; let mut layout = TranspileLayout::new(None, Some(first), initial_qubits, 4); - layout.compose_output_permutation(&second, true); + layout.add_permutation_outside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(1), Qubit(2), Qubit(3), Qubit(0)].as_slice()); assert_eq!(expected, result); + let first = vec![Qubit(1), Qubit(2), Qubit(3), Qubit(0)]; - let second = vec![Qubit(0), Qubit(2), Qubit(1), Qubit(3)]; + let second = [0, 2, 1, 3].map(Qubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; let mut layout = TranspileLayout::new(None, Some(first), initial_qubits, 4); - layout.compose_output_permutation(&second, false); + layout.add_permutation_inside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(2), Qubit(1), Qubit(3), Qubit(0)].as_slice()); assert_eq!(expected, result); @@ -1053,10 +1164,10 @@ mod test_transpile_layout { #[test] fn test_compose_no_permutation_original() { - let second = vec![Qubit(2), Qubit(3), Qubit(1), Qubit(0)]; + let second = [2, 3, 1, 0].map(Qubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; let mut layout = TranspileLayout::new(None, None, initial_qubits, 4); - layout.compose_output_permutation(&second, false); + layout.add_permutation_inside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(2), Qubit(3), Qubit(1), Qubit(0)].as_slice()); assert_eq!(expected, result); @@ -1064,10 +1175,10 @@ mod test_transpile_layout { #[test] fn test_compose_no_permutation_second() { - let second = [Qubit(2), Qubit(3), Qubit(1), Qubit(0)]; + let second = [2, 3, 1, 0].map(Qubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; let mut layout = TranspileLayout::new(None, None, initial_qubits, 4); - layout.compose_output_permutation(&second, true); + layout.add_permutation_outside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(2), Qubit(3), Qubit(1), Qubit(0)].as_slice()); assert_eq!(expected, result); diff --git a/crates/transpiler/src/transpiler.rs b/crates/transpiler/src/transpiler.rs index c41c23c0fd04..101ab8b3c012 100644 --- a/crates/transpiler/src/transpiler.rs +++ b/crates/transpiler/src/transpiler.rs @@ -24,7 +24,8 @@ use crate::transpile_layout::TranspileLayout; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::converters::dag_to_circuit; use qiskit_circuit::dag_circuit::DAGCircuit; -use qiskit_circuit::{PhysicalQubit, Qubit}; +use qiskit_circuit::nlayout::NLayout; +use qiskit_circuit::{PhysicalQubit, Qubit, VirtualQubit}; #[derive(Copy, Eq, PartialEq, Debug, Clone)] #[repr(u8)] @@ -100,8 +101,7 @@ pub fn transpile( ) { if let Some((new_dag, permutation)) = run_elide_permutations(&dag)? { dag = new_dag; - let permutation: Vec = permutation.into_iter().map(Qubit::new).collect(); - transpile_layout.compose_output_permutation(&permutation, false); + transpile_layout.add_permutation_inside(|q| Qubit::new(permutation[q.index()])); }; run_remove_diagonal_before_measure(&mut dag); run_remove_identity_equiv(&mut dag, approximation_degree, Some(target)); @@ -117,8 +117,8 @@ pub fn transpile( )?; if let Some(result) = result { dag = result.0; - let split_2q_permutation = result.1.into_iter().map(Qubit::new).collect::>(); - transpile_layout.compose_output_permutation(&split_2q_permutation, true); + let permutation = result.1; + transpile_layout.add_permutation_inside(|q| Qubit::new(permutation[q.index()])); } } // layout stage @@ -175,23 +175,8 @@ pub fn transpile( false, )?; dag = result; - transpile_layout.initial_layout = Some(initial_layout); - let routing_permutation: Vec = (0..dag.num_qubits() as u32) - .map(|ref q| { - Qubit( - final_layout - .virtual_to_physical( - transpile_layout - .initial_layout - .as_ref() - .unwrap() - .physical_to_virtual(PhysicalQubit(*q)), - ) - .0, - ) - }) - .collect(); - transpile_layout.compose_output_permutation(&routing_permutation, true); + transpile_layout = + layout_from_sabre_result(&dag, initial_layout, &final_layout, &transpile_layout); } } else if optimization_level == OptimizationLevel::Level2 { if let Some(vf2_result) = @@ -216,23 +201,8 @@ pub fn transpile( false, )?; dag = result; - transpile_layout.initial_layout = Some(initial_layout); - let routing_permutation: Vec = (0..dag.num_qubits() as u32) - .map(|ref q| { - Qubit( - final_layout - .virtual_to_physical( - transpile_layout - .initial_layout - .as_ref() - .unwrap() - .physical_to_virtual(PhysicalQubit(*q)), - ) - .0, - ) - }) - .collect(); - transpile_layout.compose_output_permutation(&routing_permutation, true); + transpile_layout = + layout_from_sabre_result(&dag, initial_layout, &final_layout, &transpile_layout); } } else if let Some(vf2_result) = vf2_layout_pass( &dag, @@ -262,24 +232,8 @@ pub fn transpile( false, )?; dag = result; - transpile_layout.initial_layout = Some(initial_layout); - let routing_permutation: Vec = (0..dag.num_qubits() as u32) - .map(|ref q| { - Qubit( - final_layout - .virtual_to_physical( - transpile_layout - .initial_layout - .as_ref() - .unwrap() - .physical_to_virtual(PhysicalQubit(*q)), - ) - .0, - ) - }) - .collect(); - - transpile_layout.compose_output_permutation(&routing_permutation, true); + transpile_layout = + layout_from_sabre_result(&dag, initial_layout, &final_layout, &transpile_layout); } // Routing stage if optimization_level == OptimizationLevel::Level0 { @@ -288,32 +242,22 @@ pub fn transpile( // constraints in the target and returns None if the circuit conforms to the undirected // connectivity constraints if run_check_map(&dag, target).is_some() { + let initial_layout = transpile_layout + .initial_layout() + .expect("a layout pass was already called"); let (out_dag, final_layout) = sabre::sabre_routing( &dag, &routing_target, &sabre_heuristic, - transpile_layout.initial_layout.as_ref().unwrap(), + initial_layout, 5, seed, Some(true), )?; dag = out_dag; - let routing_permutation: Vec = (0..dag.num_qubits() as u32) - .map(|ref q| { - Qubit( - final_layout - .virtual_to_physical( - transpile_layout - .initial_layout - .as_ref() - .unwrap() - .physical_to_virtual(PhysicalQubit(*q)), - ) - .0, - ) - }) - .collect(); - transpile_layout.compose_output_permutation(&routing_permutation, true); + let routing_permutation = + TranspileLayout::permutation_from_layouts(initial_layout, &final_layout); + transpile_layout.add_permutation_inside(|q| routing_permutation[q.index()]); } } // Translation Stage @@ -469,6 +413,25 @@ impl MinPointState { } } +fn layout_from_sabre_result( + dag: &DAGCircuit, + initial_layout: NLayout, + final_layout: &NLayout, + old_transpile_layout: &TranspileLayout, +) -> TranspileLayout { + let mut new_transpile_layout = TranspileLayout::from_layouts( + initial_layout, + final_layout, + dag.qubits().objects().clone(), + old_transpile_layout.num_input_qubits(), + ); + if let Some(old_permutation) = old_transpile_layout.output_permutation() { + new_transpile_layout + .add_permutation_outside(|q| VirtualQubit(old_permutation[q.index()].0)); + } + new_transpile_layout +} + #[cfg(all(test, not(miri)))] mod tests { use super::*; From bcb5fcd54dcfa8e68d27e4028e758f843233275c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 27 Aug 2025 22:53:14 +0100 Subject: [PATCH 19/22] Fix typing of tests --- crates/transpiler/src/transpile_layout.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/transpiler/src/transpile_layout.rs b/crates/transpiler/src/transpile_layout.rs index a33a86106402..2f44206845c0 100644 --- a/crates/transpiler/src/transpile_layout.rs +++ b/crates/transpiler/src/transpile_layout.rs @@ -564,7 +564,7 @@ impl TranspileLayout { mod test_transpile_layout { use super::TranspileLayout; use qiskit_circuit::bit::ShareableQubit; - use qiskit_circuit::nlayout::{NLayout, PhysicalQubit}; + use qiskit_circuit::nlayout::{NLayout, PhysicalQubit, VirtualQubit}; use qiskit_circuit::Qubit; #[test] @@ -1144,9 +1144,14 @@ mod test_transpile_layout { #[test] fn test_compose() { let first = vec![Qubit(0), Qubit(3), Qubit(1), Qubit(2)]; - let second = [2, 3, 1, 0].map(Qubit); + let second = [2, 3, 1, 0].map(VirtualQubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; - let mut layout = TranspileLayout::new(None, Some(first), initial_qubits, 4); + let mut layout = TranspileLayout::new( + Some(NLayout::generate_trivial_layout(4)), + Some(first), + initial_qubits, + 4, + ); layout.add_permutation_outside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(1), Qubit(2), Qubit(3), Qubit(0)].as_slice()); @@ -1175,9 +1180,14 @@ mod test_transpile_layout { #[test] fn test_compose_no_permutation_second() { - let second = [2, 3, 1, 0].map(Qubit); + let second = [2, 3, 1, 0].map(VirtualQubit); let initial_qubits = vec![ShareableQubit::new_anonymous(); 4]; - let mut layout = TranspileLayout::new(None, None, initial_qubits, 4); + let mut layout = TranspileLayout::new( + Some(NLayout::generate_trivial_layout(4)), + None, + initial_qubits, + 4, + ); layout.add_permutation_outside(|q| second[q.index()]); let result = layout.output_permutation(); let expected = Some([Qubit(2), Qubit(3), Qubit(1), Qubit(0)].as_slice()); From f0251fbbe556442eb10db6fa7da9bf6080a0713d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 28 Aug 2025 07:56:57 -0400 Subject: [PATCH 20/22] Add QkTranspileResult to the correct docs section --- crates/cext/src/transpiler/transpile_function.rs | 1 - docs/cdoc/index.rst | 2 +- docs/cdoc/qk-transpiler.rst | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/cext/src/transpiler/transpile_function.rs b/crates/cext/src/transpiler/transpile_function.rs index 9097f809bfb0..cb0d6be5a00f 100644 --- a/crates/cext/src/transpiler/transpile_function.rs +++ b/crates/cext/src/transpiler/transpile_function.rs @@ -21,7 +21,6 @@ use qiskit_transpiler::transpile_layout::TranspileLayout; use crate::exit_codes::ExitCode; use crate::pointers::const_ptr_as_ref; -/// @ingroup QkTranspiler /// The container result object from ``qk_transpile`` /// /// When the transpiler successfully compiles a quantum circuit for a given target it diff --git a/docs/cdoc/index.rst b/docs/cdoc/index.rst index a4207b6588bf..e9e9ee28f158 100644 --- a/docs/cdoc/index.rst +++ b/docs/cdoc/index.rst @@ -61,7 +61,7 @@ results transpiling circuits from Python via the C API. .. toctree:: :maxdepth: 1 - qk-transpiler.rst + qk-transpiler qk-target qk-target-entry qk-transpile-layout diff --git a/docs/cdoc/qk-transpiler.rst b/docs/cdoc/qk-transpiler.rst index 04bb58a80e05..13588a24e8c9 100644 --- a/docs/cdoc/qk-transpiler.rst +++ b/docs/cdoc/qk-transpiler.rst @@ -14,6 +14,9 @@ should call the :py:func:`.generate_preset_pass_manager` or Data Types ========== +.. doxygenstruct:: QkTranspileResult + :members: + .. doxygenstruct:: QkTranspileOptions :members: From 3d28043b101e2f730c23c110befcdf1166b915b4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 28 Aug 2025 07:58:12 -0400 Subject: [PATCH 21/22] Fix docs typo in transpiler page preamble --- docs/cdoc/qk-transpiler.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cdoc/qk-transpiler.rst b/docs/cdoc/qk-transpiler.rst index 13588a24e8c9..50a7c9bd9d72 100644 --- a/docs/cdoc/qk-transpiler.rst +++ b/docs/cdoc/qk-transpiler.rst @@ -2,7 +2,7 @@ QkTranspiler ============ -The :c:func:`qk_transpiler` function exposes Qiskit's tranpsiler (:py:mod:`qiskit.transpiler`) to C. +The :c:func:`qk_transpile` function exposes Qiskit's tranpsiler (:py:mod:`qiskit.transpiler`) to C. The basic functionality is using the same underlying code as the Python-space version, but the transpiler as exposed to C has more limitations than what is exposed to Python. The transpiler assumes a circuit built constructed using solely the C API and is intended to From 07bea180066f2e194b6accaf25c323aa371407ed Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 28 Aug 2025 09:24:22 -0400 Subject: [PATCH 22/22] Fix merge conflict with function rename in tests --- test/c/test_transpiler.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/c/test_transpiler.c b/test/c/test_transpiler.c index e67292aa3676..e075a81edcd8 100644 --- a/test/c/test_transpiler.c +++ b/test/c/test_transpiler.c @@ -145,7 +145,7 @@ int test_transpile_bv(void) { transpile_cleanup: qk_circuit_free(transpile_result.circuit); qk_transpile_layout_free(transpile_result.layout); - qk_opcounts_free(op_counts); + qk_opcounts_clear(&op_counts); circuit_cleanup: qk_circuit_free(qc);