Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
104ac8d
Add transpiler C API
mtreinish Jul 18, 2025
4ad3154
Add docs files
mtreinish Aug 26, 2025
d7ca189
Fix routing and level 0
mtreinish Aug 26, 2025
53a5d5b
Fix clippy
mtreinish Aug 26, 2025
c3bb0d5
Merge branch 'main' into transpile-c-wip
mtreinish Aug 26, 2025
6efbff0
Fix VF2 layout allocation with idle qubits
mtreinish Aug 26, 2025
6e831b6
Remove commented out vf2postlayout code
mtreinish Aug 27, 2025
5c96d44
Run idle qubits test
mtreinish Aug 27, 2025
e2426f4
Add release note
mtreinish Aug 27, 2025
3bbd9d3
Move MinPointState definition outside transpile func and simplify int…
mtreinish Aug 27, 2025
57a7115
Merge branch 'main' into transpile-c-wip
mtreinish Aug 27, 2025
1a89102
Apply suggestions from code review
mtreinish Aug 27, 2025
e5cbe55
Docs updates
mtreinish Aug 27, 2025
3a29e4c
Allow error to be null and just don't write out error msg
mtreinish Aug 27, 2025
85597f3
Use an enum for optimization level
mtreinish Aug 27, 2025
16b6a98
Fix c test issues
mtreinish Aug 27, 2025
7e6c5ba
Use ExitCode enum for C return type
mtreinish Aug 27, 2025
35915b6
Add comment about run_check_map return
mtreinish Aug 27, 2025
9986cb9
Fix rust tests
mtreinish Aug 27, 2025
bea12c4
Add permutation-mutation methods to `TranspileLayout`
jakelishman Aug 27, 2025
bcb5fcd
Fix typing of tests
jakelishman Aug 27, 2025
f0251fb
Add QkTranspileResult to the correct docs section
mtreinish Aug 28, 2025
3d28043
Fix docs typo in transpiler page preamble
mtreinish Aug 28, 2025
22ead91
Merge branch 'main' into transpile-c-wip
mtreinish Aug 28, 2025
07bea18
Fix merge conflict with function rename in tests
mtreinish Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/cext/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ prefix_with_name = true
"VF2LayoutResult" = "QkVF2LayoutResult"
"ElidePermutationsResult" = "QkElidePermutationsResult"
"TranspileLayout" = "QkTranspileLayout"
"TranspileResult" = "QkTranspileResult"
"TranspileOptions" = "QkTranspileOptions"
2 changes: 2 additions & 0 deletions crates/cext/src/exit_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArithmeticError> for ExitCode {
Expand Down
1 change: 1 addition & 0 deletions crates/cext/src/transpiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@

pub mod passes;
pub mod target;
pub mod transpile_function;
pub mod transpile_layout;
199 changes: 199 additions & 0 deletions crates/cext/src/transpiler/transpile_function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// 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::exit_codes::ExitCode;
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. 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,
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 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
/// 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
///
/// 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 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 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
/// @params options A pointer to an options object that defines user options. If this is a null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// @params options A pointer to an options object that defines user options. If this is a null
/// @params options A pointer to a ``QkTranspileOptions`` 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. 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// If the transpiler fails a pointer to the string with the error description will be written
/// If the transpiler fails, a pointer to the string with the error description will be written

Also, should we adopt this pattern with the standalone passes in C?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think after we resolve the error handling story for C. Right now this pattern is kind of future facing and we just stuff a backtrace in the error string as that's the best we can do.

/// 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``, 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(
qc: *const CircuitData,
target: *const Target,
options: *const TranspileOptions,
result: *mut TranspileResult,
error: *mut *mut c_char,
Comment thread
Cryoris marked this conversation as resolved.
) -> 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) };
let options = unsafe { const_ptr_as_ref(options) };
if !(0..=3u8).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)
};
Comment thread
jakelishman marked this conversation as resolved.
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 {
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 ExitCode::TranspilerError;
}
}

match transpile(
qc,
target,
options.optimization_level.into(),
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)),
};
}
ExitCode::Success
}
Err(e) => {
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();
}
}
ExitCode::TranspilerError
}
}
}
2 changes: 1 addition & 1 deletion crates/cext/src/transpiler/transpile_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
pub fn size(&self, recurse: bool) -> PyResult<usize> {
let mut length = self.num_ops();
if !self.has_control_flow() {
return Ok(length);
Expand Down Expand Up @@ -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<usize> {
pub fn depth(&self, recurse: bool) -> PyResult<usize> {
if self.qubits.is_empty() && self.clbits.is_empty() && self.num_vars() == 0 {
return Ok(0);
}
Expand Down
1 change: 1 addition & 0 deletions crates/transpiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions crates/transpiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/transpiler/src/passes/apply_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
5 changes: 4 additions & 1 deletion crates/transpiler/src/passes/sabre/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines -19 to +27

@jakelishman jakelishman Aug 27, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely unimportant, but route probably needn't be pub at all, and anything that's pub(crate) use can/ought to be full pub use.

edit: we can follow up on this - we're going to need to rework the Sabre interfaces to use TranspileLayout anyway.


pub fn sabre(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(route::sabre_routing))?;
Expand Down
2 changes: 1 addition & 1 deletion crates/transpiler/src/passes/sabre/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ pub struct PyRoutingTarget(pub Option<RoutingTarget>);
#[pymethods]
impl PyRoutingTarget {
#[staticmethod]
fn from_target(target: &Target) -> PyResult<Self> {
pub(crate) fn from_target(target: &Target) -> PyResult<Self> {
Comment on lines 316 to +319

@jakelishman jakelishman Aug 27, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add the from_target method to RoutingTarget and have that be properly pub and pub use'd, rather than exposing PyRoutingTarget to the rest of Rust.

edit: we can follow up on this - we're going to need to rework the Sabre interfaces to use TranspileLayout anyway.

let coupling = match target.coupling_graph() {
Ok(coupling) => coupling,
Err(TargetCouplingError::AllToAll) => return Ok(Self(None)),
Expand Down
Loading
Loading