Safeguard
Supply Chain Security•

How Quantum Programs Are Compiled: OpenQASM 3 and Qubit Routing

A quantum compiler maps circuits onto a chip's qubits, inserts SWAPs and rewrites gates into native ones. OpenQASM 3, routing and the open source SDKs.

Safeguard Security Team
40 min read

A quantum program is compiled in stages. You write a circuit in an SDK such as Qiskit, Cirq, PennyLane or pytket, it is held in an intermediate representation such as OpenQASM 3 or QIR, and a transpiler maps its logical qubits onto physical ones, inserts SWAP gates where the chip's wiring does not connect two qubits, rewrites every gate into the hardware's native set and optimizes the result. A control layer then turns those gates into timed microwave or laser pulses.

The compiler matters more than on a classical computer, because every extra two qubit gate adds noise. A 3 qubit GHZ circuit that needs 2 two qubit gates on a well chosen layout needs 5 on a poorly chosen one. This post covers each layer, OpenQASM 3, layout and routing with SABRE, the open source SDKs, a worked example with real transpiler output, and how to pin and inventory the toolchain. It goes one level deeper than our explainers on what quantum computing is and how a QPU works.

Key takeaways

  • The stack has five layers: an SDK where you build circuits, an intermediate representation (IR) that carries them, a transpiler that fits them to one device, a pulse or control layer that schedules physical signals, and the hardware.
  • OpenQASM 3 adds real time classical control flow (if, for, while, switch), extern calls into classical code, explicit timing (durations, stretch, delay, box) and pulse level calibrations (defcal with the OpenPulse grammar) to the straight line circuits of OpenQASM 2. The repository lists 3.1 as the current version, and the live specification already marks some additions as version 3.2.
  • Qiskit's prebuilt transpiler runs six stages: init, layout, routing, translation, optimization and scheduling. Its documentation calls optimal SWAP insertion NP-hard and uses SABRE, a stochastic heuristic from a 2018 paper, so the output depends on a seed.
  • Native gate sets differ. IBM's published instruction table lists CZ and ECR as two qubit gates; IonQ's docs describe Mølmer-Sørensen gates on Aria-class systems and ZZ gates on Forte-class systems. Depth figures from different gate sets are not comparable.
  • QIR, from the QIR Alliance, is an LLVM based IR with profiles (Base for measure at the end, Adaptive for branching on mid circuit results). MLIR based toolchains such as PennyLane's Catalyst compile hybrid classical and quantum code.
  • The four SDKs are Apache 2.0 open source projects. In our 25 October 2026 test, clean installs pulled 7 packages (qiskit 2.6.1), 21 (cirq-core 1.7.0), 22 (pennylane 0.45.1) and 13 (pytket 2.18.5). Pin them with hashes and keep an SBOM.

What does the quantum software stack look like?

Even the simplest job passes through several pieces of software, each turning a more abstract description into a more physical one.

The SDK. A Python library where you build, parameterize and submit circuits. This post covers four open source SDKs: Qiskit, Cirq, PennyLane and pytket.

The circuit IR. An intermediate representation is a precise, serializable description of the program that tools can exchange. OpenQASM is a text language for circuits that can be written by hand or produced by tools. QIR is a set of conventions on top of LLVM IR. SDKs also keep their own in-memory IRs; Qiskit's README says its internal data model is written in Rust and exposed through a C API since Qiskit 2.0.

The transpiler or compiler. This is the part that knows about one specific device. It reads the device's coupling map (which physical qubits can interact) and its native instructions, then rewrites the circuit so it uses only those. IBM's documentation calls the result an ISA circuit, one that obeys the instruction set architecture of a particular backend.

The pulse or control layer. Gates are not what the hardware executes. On superconducting chips a gate becomes a shaped microwave pulse on a drive line at a calibrated frequency; on trapped ions it becomes a laser pulse sequence. OpenQASM 3 can describe this layer itself through defcal blocks written in the OpenPulse grammar, with ports, frames, waveforms and play and capture instructions.

The hardware. Control electronics turn waveforms into analogue signals and digitize the readout. Our QPU explainer covers the chip and readout chain.

LayerWhat it holdsExamples from the projects' own docsWho usually owns it
SDKCircuits, parameters, jobsQiskit QuantumCircuit, Cirq Circuit, PennyLane QNodes, pytket CircuitYou, in your repository
Circuit IRA portable program text or bitcodeOpenQASM 2 and 3, QIR (LLVM IR)Shared standard
TranspilerDevice specific rewritingQiskit generate_preset_pass_manager, Cirq RouteCQC, pytket default_compilation_passSDK, run locally or by the provider
Pulse layerTimed waveforms on physical channelsOpenQASM 3 defcal with OpenPulse ports, frames and playHardware provider, sometimes the user
HardwareQubits, wiring, control electronicsSuperconducting, trapped ion and other platformsHardware provider

The boundaries move. Some providers compile OpenQASM 3 themselves; others expect native gates. IonQ's documentation says a circuit submitted to its cloud cannot mix native and abstract gates.

What is OpenQASM 3, and what does it add over OpenQASM 2?

OpenQASM is an open quantum assembly language. The first public version was described by Andrew Cross, Lev Bishop, John Smolin and Jay Gambetta in arXiv:1707.03429, submitted on 11 July 2017. That paper describes OpenQASM as a language for low depth circuits over "the CNOT plus SU(2) basis", written as straight line code with measurement, reset, fast feedback and gate subroutines, executable on what was then the IBM Q Experience.

OpenQASM 3 was described in arXiv:2104.14722, "OpenQASM 3: A broader and deeper quantum assembly language", submitted on 30 April 2021 and revised on 16 March 2022. The paper's key idea is two timescales of quantum and classical interaction. Near time computation, such as an optimizer choosing the next set of parameters, is already handled well by ordinary programming languages. Real time computation, which must finish within the coherence time of the qubits, needs to be tightly coupled to the quantum operations. OpenQASM 3 focuses on that real time domain. The paper says the new features form "a multi-level intermediate representation", so the same language can describe an abstract circuit, a scheduled circuit and a pulse program.

Versions and governance

The OpenQASM repository README lists the current version as 3.1, links the live specification at openqasm.com, and keeps OpenQASM 2.0 on a separate branch. The live specification already carries notes marked "Added in version 3.2", for example the nop directive. The project is licensed under Apache 2.0.

The repository's governance document defines three roles (Member, Contributor and TSC Member) and two structures, a Technical Steering Committee and working groups. The TSC has six Contributors serving two year terms, and as of 4 February 2026 its members came from hardware, control electronics and cloud companies. The document scopes this governance to OpenQASM itself, the specification and reference parsers, not to Qiskit as a whole. A reference Python parser, the openqasm3 package on PyPI, is published as "Reference OpenQASM AST in Python" under Apache 2.0; its latest release on PyPI is 1.0.1 from February 2025.

The specification says it "does not define the execution environment that accepts OpenQASM as input". Hardware may accept only a subset at run time, for example rejecting int[5] or float[16] declarations, while compilers must fold operations on compile time constants such as sin of a literal. Two implementations that accept a program should behave the same, except where the specification allows differences.

Gates and modifiers

OpenQASM 3 has two built-in gates: the one qubit gate U(θ, φ, λ) and the zero qubit global phase gate gphase(γ). Everything else is defined in terms of them. The standard library file stdgates.inc defines the familiar gates, and a Hadamard is written as a U rotation plus a global phase. The specification notes that U, gphase and CNOT form a universal set, but that this basis "is not an enforced compilation target but a mechanism to define other gates". In other words, the language does not force any hardware to use it.

Gate modifiers are new in version 3:

  • ctrl @ makes a controlled version of a gate. The library defines CX as ctrl @ X.
  • negctrl @ makes a gate controlled on the value 0 rather than 1.
  • inv @ replaces a gate with its inverse, computed by reversing its definition.
  • pow(k) @ raises a gate to a power, which for integer k can be implemented by repetition.

Qubits can be virtual (declared with qubit[3] q;) or physical. Physical qubits are written $0, $1 and so on, numbered by the hardware provider's published topology. The specification says physical qubits "cannot be remapped by a compiler or hardware provider without opt-in from the programmer". That one rule is the language level hook for layout: a program written on virtual qubits invites the compiler to choose a layout, and a program on physical qubits pins it.

Classical control flow

OpenQASM 3 adds classical types (bit, int, uint, float, angle, bool, complex and arrays), and control flow: if and else, for loops over ranges and sets of values, while loops, and a switch statement. Subroutines are defined with def, and extern declares a classical function whose body lives outside the program, with its signature given in the source. Measurement results can feed straight into branches, which is how mid circuit measurement and feedforward are expressed:

OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit c;
h q[0];
c = measure q[0];
if (c) {
  x q[1];
}

The IBM documentation for Qiskit's OpenQASM 3 support shows a longer example with a while loop that repeats a block until two mid circuit measurements change. It also says that support is still "in an exploratory phase", that importing needs the separate qiskit-qasm3-import package, and that qiskit.qasm3.dumps() exports a circuit as a string.

Timing: durations, stretch, delay and box

OpenQASM 2 had no notion of time beyond gate order. OpenQASM 3 adds a duration type with units of ns, us (or µs), ms, s and dt, where dt is "the duration of one waveform sample on the backend". A stretch is a duration the compiler may grow to satisfy constraints, so you can say "spread these idle periods evenly" without knowing gate lengths in advance. delay[100ns] q[0]; idles a qubit, durationof() measures a block, and box groups instructions so the compiler treats them as a unit with a duration. The specification credits the idea of boxes and stretch to the "boxes and glue" of the TeX typesetting system. The barrier instruction, inherited from OpenQASM 2, stops the compiler from moving gates across it.

This matters for dynamical decoupling: with stretch, a circuit states the intent and the scheduler sets the exact spacing.

Pulse level calibrations

The deepest layer is calibration. A defcalgrammar "openpulse"; declaration selects a pulse grammar, cal blocks carry setup code in that grammar, and defcal defines how a gate or measurement is carried out on specific physical qubits, for example defcal rz(angle[20] theta) $0 { ... }. The specification draws a sharp line: gate definitions describe unitary transformations that the compiler may rewrite, while defcal definitions are entries in a lookup table that tell the compiler how to implement an operation on given hardware.

Inside OpenPulse, a port is the software name for an input or output channel, which may map to a digital to analogue converter or a more complex chain. A frame tracks time and the phase and frequency of the carrier, so programs do not have to track absolute time. A waveform is the pulse envelope, the play instruction schedules a waveform on a frame, and capture schedules an acquisition. The specification says capture is "a special extern function which is specified by a hardware vendor", because measurement differs so much between platforms.

FeatureOpenQASM 2 (2017 paper)OpenQASM 3 (2021 paper, live spec 3.1)
Program shapeStraight line codeStructured code with branches and loops
Classical dataBit registersbit, int, uint, float, angle, bool, complex, arrays, const
Feedbackif on a classical register valueif, else, for, while, switch, subroutines with def
External classical codeNoneextern function declarations
Gate definitionsgate from U and CXgate from U and gphase, plus ctrl @, negctrl @, inv @, pow(k) @
Physical qubitsNot distinguished$n syntax, not remappable without opt-in
TimingNoneduration, stretch, delay, box, durationof, units down to dt
Pulse levelOpaque gatesdefcalgrammar, cal, defcal, OpenPulse ports, frames, waveforms, play, capture
Input and outputNoneinput and output declarations for parameters and results

How are circuits decomposed into native gate sets?

Every device supports a small set of physical operations. The translation step rewrites every gate in the circuit into that set. Two consequences follow. First, gate counts and depth grow, often a lot. Second, the native set shapes which optimizations are worth doing.

Superconducting qubits: the IBM instruction table

IBM's QPU information guide publishes a table of supported instructions. Single qubit gates are RZ, SX, X, XSlow, ID and delay. Two qubit gates are CZ and ECR (echoed cross resonance). Fractional gates, available when a backend is loaded with use_fractional_gates=True, are RX on one qubit and RZZ on two. Non-unitary instructions include measure and reset variants, and control flow is if_else for classical feedforward. The same guide shows sample output for a Heron r2 backend whose supported instructions include cz, and a sample cz entry with a duration of 6.8e-08 seconds, that is 68 nanoseconds, and an error of about 0.0077 on one qubit pair. Those numbers are one sample and change with each calibration.

IBM's processor types page describes Heron as a 156 qubit family: revision r1 (December 2023) had 133 qubits, r2 (July 2024) was redesigned to 156 qubits on a heavy hexagonal lattice, and r3 followed in July 2025. It describes Nighthawk as a grid topology with 120 programmable qubits. The coupling map is part of the target: on a heavy hexagonal lattice most qubits have two or three neighbours, so most pairs in a circuit are not directly connected.

Because the single qubit basis is RZ, SX and X, an arbitrary one qubit rotation is rewritten as a short pattern of RZ and SX gates. In our worked example below, a Hadamard became rz(pi/2), sx, rz(pi/2).

Trapped ions: Mølmer-Sørensen and ZZ gates

IonQ's guide to native gates lists GPi and GPi2 single qubit gates, a "virtual Z" handled by phase accounting, and two entangling options: the Mølmer-Sørensen (MS) gate on Aria-class systems and the ZZ gate on Forte-class systems. The guide says the MS gate couples the internal states of two ions through the chain's shared modes of motion, that it can act on any two ions in the chain, and that only two qubit MS gates are offered even though global MS on many qubits is possible. A fully entangling MS gate is an XX rotation; partially entangling versions take an extra angle. The guide gives a CNOT decomposition built from an XX(π/4) interaction plus RX and RY rotations, and says the vendor supports Qiskit's transpiler for converting abstract circuits to its native gates.

Because any two ions in a chain can be entangled directly, routing needs no SWAPs inside one chain, but every CNOT still becomes an MS or ZZ gate plus single qubit rotations.

Why the IR does not pick the gates

Because OpenQASM 3's built-in gates are a definitional basis rather than a target, a compiler for a CZ device and one for an MS device read the same cx line and emit different physical sequences. "How many gates" has no single answer until you name the target.

Target familyTwo qubit native gates (per vendor docs)Connectivity effectWhat translation does to a CNOT
IBM superconducting (instruction table)CZ, ECR; RZZ as a fractional gateFixed lattice, mostly two or three neighbours per qubitCZ plus single qubit SX and RZ rotations around the target
IonQ trapped ion, Aria-classMS (XX)Any two ions in a chainXX(π/4) plus RX and RY rotations
IonQ trapped ion, Forte-classZZAny two ions in a chainZZ interaction plus single qubit rotations
Abstract simulatorAnything the SDK supportsAll to all, unless you set a coupling mapOften no change at all

How do layout and routing work on limited connectivity?

On most superconducting chips a two qubit gate is only possible between qubits that are physically coupled. The compiler must solve two related problems. Layout chooses which physical qubit each virtual qubit starts on. Routing inserts SWAP gates during the circuit so that, whenever a two qubit gate runs, its qubits are neighbours.

Qiskit's six stages

IBM's guide to transpiler stages describes the prebuilt pipeline as six stages run by a StagedPassManager:

  1. init translates gates on three or more qubits into one and two qubit gates, because most layout and routing algorithms only handle those.
  2. layout maps virtual qubits to physical qubits. It first looks for a "perfect" layout that needs no SWAPs using VF2Layout, which treats layout as a subgraph isomorphism problem solved by the VF2++ algorithm and scores candidates by average error. If none exists, SabreLayout searches heuristically from random starting layouts.
  3. routing inserts SWAPs so every two qubit gate acts on coupled qubits.
  4. translation rewrites gates into the backend's native basis.
  5. optimization combines and cancels gates, looping until a condition such as fixed depth is met.
  6. scheduling places instructions in time and accounts for idle periods, if a scheduling method is set.

The guide is blunt about routing: finding the minimum number of SWAPs "is an NP-hard problem", so Qiskit uses "a stochastic heuristic algorithm called SabreSwap", and because it is stochastic, "the circuits generated are not guaranteed to be the same over repeated runs". It notes that a SWAP not native to the backend costs three CNOTs, and that a Toffoli may need up to six CNOTs plus single qubit gates.

SABRE in plain language

SABRE comes from Gushu Li, Yufei Ding and Yuan Xie, arXiv:1809.02573, submitted on 7 September 2018 and published at ASPLOS 2019. The name stands for SWAP-based BidiREctional heuristic search. The abstract names three ideas:

  • A front layer and a cost function. At each step the algorithm looks at the front layer, the gates whose predecessors have all run. Gates whose qubits are adjacent run at once. For the rest, it scores candidate SWAPs on the qubits involved by how much they reduce the distance between qubits that need to meet. The full paper adds look ahead through an "extended set" of upcoming gates.
  • Reverse traversal for the initial layout. SABRE routes the circuit forward, then routes the reversed circuit starting from where the forward pass ended, and uses the final mapping as the starting layout. A layout that suits the end of the circuit and its start tends to need fewer SWAPs overall.
  • A decay effect. Recently swapped qubits receive a penalty, which spreads SWAPs across different qubits so more of them can run in parallel. That trades depth against total gate count.

Qiskit's SabreLayout starts from a random initial layout and repeatedly runs SabreSwap, and higher optimization levels add more trials, which is why the seed_transpiler argument changes results.

Other routers

pytket's compiler manual describes the same two problems as placement and routing. DefaultMappingPass combines the GraphPlacement strategy with routing; CXMappingPass then decomposes the inserted SWAP and BRIDGE gates into CX gates. A BRIDGE performs a CNOT between two qubits that share a neighbour using four CX gates and leaves the layout unchanged, which is sometimes cheaper than a SWAP followed by a CX.

Cirq implements routing as a transformer class, RouteCQC, built from a networkx graph of the device. Its documentation describes a heuristic that walks the circuit's time steps left to right, runs whatever gates are executable and ranks candidate SWAPs with a cost function that looks ahead up to lookahead_radius steps. A tag_inserted_swaps option marks the SWAPs the router added.

The optimization passes that follow

Routing and translation make circuits longer, and optimization tries to win some of it back. IBM's guide on optimization levels lists what each level adds:

Qiskit optimization levelLayout and routingOptimization passes named in the docs
0TrivialLayout (same numbers), SWAPs from SabreSwapBasic translation only; used for hardware characterization
1Tries TrivialLayout, then a minimum SWAP layout, then VF2PostLayout to choose better qubitsInverseCancellation, single qubit gate optimization
2Level 1 without trivial layout, with more search depth and trialsAdds CommutativeCancellation
3Level 2 with still more effortResynthesis of two qubit blocks with the KAK decomposition, OptimizeSwapBeforeMeasure, RemoveDiagonalGatesBeforeMeasure

The level 3 passes OptimizeSwapBeforeMeasure and RemoveDiagonalGatesBeforeMeasure are described as "unitarity-breaking": they change the unitary but not the measured outcome, which is fine for sampling but not if you need the exact final state.

pytket's manual describes FullPeepholeOptimise as a general pass (Clifford simplifications, single qubit gate commutation and squashing of subcircuits up to three qubits) that assumes no device constraints, so it belongs before mapping, while the SynthesiseX passes keep connectivity intact after mapping. Its default_compilation_pass offers optimisation levels with the same trade of compile time for fewer gates.

What other IRs exist, and why do IRs matter for portability?

OpenQASM is a source level language for circuits. Other IRs take different positions.

QIR. The QIR specification "defines an LLVM-based intermediate representation for quantum programs". Its motivation section makes the portability argument directly: with one IR, language developers write one compiler and hardware developers write one code generator. It describes compilation in three phases, a language specific front end, a generic phase of analysis and transformation on the IR, and a target specific back end. The specification does not fix a gate set, nor even require the gate model. Instead it defines profiles. The Base Profile covers the minimal case where measurements come at the end of the program. The Adaptive Profile extends it with control flow based on mid circuit measurement results, at minimum forward branching using LLVM branch instructions. The QIR Alliance describes itself as a community effort and holds weekly community calls on the Unitary Fund Discord.

MLIR based dialects. MLIR is LLVM's framework for building IRs with multiple levels, or dialects. PennyLane's Catalyst describes itself as an experimental package for just in time compilation of hybrid quantum and classical programs with a @qjit decorator, including optimization loops, and documents its MLIR based compiler. CUDA-Q, an Apache 2.0 project, describes a Python kernel language and its own compiler dialects in its README. The appeal of MLIR is that classical control and quantum operations live in one compiler, which is the same real time coupling OpenQASM 3 aims for at the language level.

IRFormClassical controlWho maintains itTypical role
OpenQASM 2TextSimple if on a registerOpenQASM project (branch)Legacy exchange format
OpenQASM 3TextFull: loops, branches, subroutines, externOpenQASM TSCExchange format and provider input; pulse level calibrations
QIRLLVM IR with profilesProfile dependent, from none to branchingQIR AllianceCompiler back end target
MLIR dialectsMLIRFull classical compilerEach projectHybrid JIT compilation inside one toolchain

Portability is the payoff: a circuit exported to OpenQASM 3 from one SDK can be read by another and compiled for a different device. The catch is that hardware may reject features it cannot run in real time.

Which open source SDKs exist, under which licences and governance?

All four SDKs below are open source projects under the Apache License 2.0. The versions and dates come from PyPI on 25 October 2026.

Qiskit. Started at IBM and developed in the Qiskit GitHub organization. The qiskit package was at 2.6.1, released on 22 October 2026, requiring Python 3.10 or newer. Its required dependencies are numpy (2.0 or newer, below 3), scipy, rustworkx, dill, stevedore and typing-extensions; visualization, OpenQASM 3 import and a few optional passes sit in extras. Its README describes two public APIs, Python and C, with the core in Rust and a minimum Rust compiler version of 1.91 for building from source.

Cirq. Maintained in the quantumlib GitHub organization by Google Quantum AI. Distributed as cirq and component packages such as cirq-core, which was at 1.7.0, released on 29 June 2026, requiring Python 3.11 or newer. cirq-core depends on attrs, duet, matplotlib, networkx, numpy, pandas, scipy, sympy and tqdm.

PennyLane. From Xanadu, which its README credits, built around differentiable programming of quantum circuits. pennylane was at 0.45.1, released on 26 June 2026, requiring Python 3.11 or newer. Its dependencies include pennylane-lightning (its simulator plugin), autograd, rustworkx, networkx, scipy, numpy and requests, and it pins autoray to exactly 0.8.4.

pytket. The Python interface to TKET, a C++ compiler from Quantinuum; the repository has moved to the quantinuum GitHub organization. pytket was at 2.18.5, released on 8 October 2026, requiring Python 3.10 or newer, with dependencies including sympy, numpy, lark, scipy, networkx, graphviz, jinja2 and qwasm.

SDKLicenceLatest on PyPI (25 Oct 2026)PythonPackages in a clean install (our test)Compiler entry point
QiskitApache 2.02.6.1 (22 Oct 2026)3.10+7generate_preset_pass_manager, transpile
Cirq (cirq-core)Apache 2.01.7.0 (29 Jun 2026)3.11+21Transformers such as RouteCQC
PennyLaneApache 2.00.45.1 (26 Jun 2026)3.11+22Transforms and devices; Catalyst @qjit
pytketApache 2.02.18.5 (8 Oct 2026)3.10+13default_compilation_pass, get_compiled_circuit

The install counts are what pip freeze listed after installing each pinned SDK into an empty virtual environment on macOS with Python 3.14. Counts differ by platform, Python version and extras, and a real project with provider plugins, notebooks and plotting reaches several dozen packages.

These projects interoperate rather than compete: IonQ's docs offer native gate guides for Qiskit, Cirq and PennyLane, pytket has extension packages for other SDKs' backends, and all of them read or write OpenQASM.

Worked example: a 3 qubit GHZ circuit on a linear coupling map

The GHZ state puts three qubits into an equal superposition of all zeros and all ones. There are two common ways to write it. The fan-out form applies H to qubit 0, then CNOT from qubit 0 to qubit 1 and from qubit 0 to qubit 2. The chain form applies H to qubit 0, then CNOT from 0 to 1 and from 1 to 2. Both produce the same state.

Now give the compiler a linear device, three qubits in a line where physical qubit 0 couples only to 1, and 1 couples to 0 and 2. We ran this with Qiskit 2.6.1 on 25 October 2026, with a basis of cx, rz, sx and x, and a fixed seed.

from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap, generate_preset_pass_manager
from qiskit.qasm3 import dumps

line = CouplingMap.from_line(3)          # 0 - 1 - 2

ghz = QuantumCircuit(3)
ghz.h(0)
ghz.cx(0, 1)
ghz.cx(0, 2)                             # fan-out form

for level, layout in [(0, "trivial"), (1, None), (3, None)]:
    kwargs = dict(optimization_level=level, coupling_map=line,
                  basis_gates=["cx", "rz", "sx", "x"], seed_transpiler=11)
    if layout:
        kwargs["layout_method"] = layout
    out = generate_preset_pass_manager(**kwargs).run(ghz)
    print(level, out.depth(), out.num_nonlocal_gates(), dict(out.count_ops()))

Step 1: count what the circuit needs

The input has one H and two CNOTs, depth 3, and virtual qubit 0 must interact with both 1 and 2.

Step 2: trivial layout, level 0

With layout_method="trivial", virtual qubit n sits on physical qubit n. The first CNOT (0 to 1) is fine. The second (0 to 2) is not, because physical 0 and 2 are not coupled. The router inserts one SWAP between physical qubits 1 and 2, then runs the CNOT on 0 and 1. Translation turns the H into rz(pi/2), sx, rz(pi/2), and the SWAP into three CNOTs. The output, exported to OpenQASM 3:

OPENQASM 3.0;
include "stdgates.inc";
rz(pi/2) $0;
sx $0;
rz(pi/2) $0;
cx $0, $1;
cx $1, $2;
cx $2, $1;
cx $1, $2;
cx $0, $1;

Depth is 8 (three single qubit gates on qubit 0, then five CNOTs one after another), and there are 5 two qubit gates. The final layout is recorded as virtual 1 on physical 2 and virtual 2 on physical 1, because the SWAP moved them. Notice the exported program uses physical qubits $0 to $2: the compiler has committed to hardware.

Step 3: let the layout stage choose, level 1 and level 3

At optimization level 1 and above, VF2Layout looks for a layout that needs no SWAPs. It finds one: put virtual qubit 0 on physical qubit 1, in the middle of the line, so it neighbours both others. Our run reported a layout of virtual 0 on physical 1, virtual 1 on physical 0 and virtual 2 on physical 2. Zero SWAPs, depth 5, and 2 two qubit gates. Level 3 matched it.

Step 4: the chain form

The chain form needs only neighbours 0 and 1, then 1 and 2, which a line provides with the trivial layout. Every level gave zero SWAPs, depth 5 and 2 CNOTs. Matching the circuit to the topology removed the routing problem.

Step 5: change the native gate set

Swap the basis to cz, rz, sx and x, which matches the CZ entry in IBM's instruction table. Each CNOT now becomes a CZ with single qubit rotations around the target.

Circuit and settingBasisSWAPs insertedTwo qubit gatesDepth
Fan-out, level 0, trivial layoutCX158
Fan-out, level 1CX025
Fan-out, level 3CX025
Chain, any levelCX025
Fan-out, level 0, trivial layoutCZ1520
Fan-out, level 1CZ028
Fan-out, level 3CZ027
Chain, level 0CZ0211
Chain, level 3CZ029

Three lessons follow. A bad layout raised the two qubit gate count from 2 to 5. The same circuit with the same SWAPs has depth 5 in a CX basis and up to 11 in a CZ basis, because depth counts single qubit layers too. And level 3 helped most where the basis needed cleanup, trimming the CZ chain from 11 to 9.

Step 6: see the seed at work on a larger circuit

A 3 qubit line has one obvious answer. Bigger problems do not. We transpiled an 8 qubit quantum Fourier transform onto a 3 by 3 grid with 30 different seeds:

from qiskit import QuantumCircuit
from qiskit.circuit.library import QFTGate
from qiskit.transpiler import CouplingMap, generate_preset_pass_manager

qc = QuantumCircuit(8)
qc.append(QFTGate(8), range(8))
grid = CouplingMap.from_grid(3, 3)
for seed in range(30):
    pm = generate_preset_pass_manager(optimization_level=1, coupling_map=grid,
                                      basis_gates=["cx", "rz", "sx", "x"],
                                      seed_transpiler=seed)
    out = pm.run(qc)
    print(seed, out.depth(), out.count_ops().get("cx", 0))

At level 1, depth ranged from 91 to 123 and the CNOT count from 104 to 113 across the 30 seeds, with 18 distinct depths. At level 3, depth ranged from 87 to 120 and CNOTs from 79 to 88. Running the same seed twice gave the identical circuit. That is the practical meaning of "stochastic heuristic": fix the seed when you need reproducibility, and try several seeds and keep the best when you need quality.

Common mistakes

Comparing depth across different gate sets. Our GHZ example has depth 5 in a CX basis and 11 in a CZ basis with the same SWAPs and the same number of two qubit gates. Compare two qubit gate counts, or compare depth only on the same target.

Ignoring transpiler randomness. SabreLayout and SabreSwap are stochastic. Without seed_transpiler, two runs of the same notebook can produce circuits with different depth, and results look like they changed for no reason. Set the seed for reproducibility; sweep seeds when you want the best circuit.

Assuming simulator results transfer to hardware. A simulator without a coupling map or noise model runs your circuit as written. On hardware it gets SWAPs, more gates and errors on each. Transpile to the real target and, where you can, simulate with its noise model before you trust a number.

Forgetting the final layout. After routing, the virtual qubit you measure may sit on a different physical qubit. SDKs record this in the circuit's layout; read results through it.

Treating "valid OpenQASM 3" as "runs on my device". The specification lets hardware reject types and real time operations it cannot support. Check the provider's supported subset before writing loops and extern calls.

Writing physical qubits too early. $0 style qubits cannot be remapped without your opt-in. Hard coding them stops the layout stage from moving your circuit to better calibrated qubits.

Leaving the toolchain unpinned. A new SDK release can change default passes, which changes depth and results. Pin the SDK, its provider plugin and the transpiler seed together.

Supply chain angle: a quantum toolchain is a Python dependency tree

Safeguard does not do quantum physics or mathematics, and its models are single purpose security models, not tools for solving equations or simulating qubits. The honest connection is narrower. Every circuit in this post was compiled by open source Python packages with their own dependencies, native extensions and release schedules. Qiskit ships compiled Rust code, PennyLane depends on the separate pennylane-lightning simulator plugin, and pytket wraps a C++ compiler. Those packages can carry known vulnerabilities and change under an unpinned install like any other codebase, and a transpiler result depends on the exact version that produced it. Pinning the toolchain makes both the security inventory and the compiled circuits reproducible.

Pin with hashes

pip's secure installs guide recommends hash checking mode with --require-hashes and disallowing source distributions with --only-binary :all:. Hash checking is all or nothing: once any requirement has a hash, every requirement must.

python3 -m venv .venv && . .venv/bin/activate
python -m pip install pip-tools
echo "qiskit==2.6.1" > requirements.in
pip-compile --generate-hashes requirements.in -o requirements.txt
python -m pip install --require-hashes --only-binary :all: -r requirements.txt

If you use uv, its uv export command can write the lock as requirements.txt, as pylock.toml (PEP 751) or as CycloneDX 1.5 JSON. pip also has an experimental pip lock command that writes pylock.toml; its documentation warns the lock file is only valid for the current Python version and platform. Our posts on pip hash checking mode and uv lock files go deeper.

Generate an SBOM for the environment you actually run

The CycloneDX project's cyclonedx-py tool can build an SBOM from a Python environment, a requirements.txt file and other project formats.

# Install the SBOM tool in its own environment, not the one you are describing
python3 -m venv /tmp/sbomtool && /tmp/sbomtool/bin/pip install cyclonedx-bom
/tmp/sbomtool/bin/cyclonedx-py environment --of JSON -o qiskit-sbom.cdx.json .venv/bin/python

The comment matters. Run inside the Qiskit environment, cyclonedx-bom produced an SBOM with 40 components, of which only 7 belonged to the quantum toolchain; the other 33 were the SBOM tool and its dependencies. Keep the tool separate and point it at the interpreter you want to describe.

Verify and scan

Commit the SBOM next to the lock file, regenerate both when you bump an SDK, and scan in CI.

safeguard scan --dir .

The Safeguard CLI scan writes JSON results under ./output, listing components, versions and known vulnerabilities, and reachability analysis ranks the vulnerable code paths your code actually calls. Remember the limit of a Python level SBOM: it lists wheels from their installed metadata, so a Rust crate or C++ library compiled into a wheel appears as part of that wheel rather than as its own component.

A quantum toolchain is mostly ordinary open source Python, so the gaps between a published fix and a deployed fix apply to it like any other codebase. The fit to the quantum hardware itself is indirect: these steps cover the packages in your repository, not the provider's control stack.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

These demos show dependency scanning and remediation on ordinary code. They do not compile circuits, choose layouts or talk to quantum hardware. You can download Safeguard and follow the CLI docs to try them on your own projects.

This is a real CLI session: install, open the interactive terminal and ask it to fix a lodash CVE. The assistant reads the finding and advisory, asks before any command that changes something, edits the dependency, runs install and tests with approval, prints a summary and pushes nothing. In a quantum project the same flow would update a vulnerable pinned package in requirements.txt, and you would rerun your transpilation tests with a fixed seed before merging.

Click through it at your own pace, or open it full screen.

This demo sets up the Safeguard runner on your own machine, approves it with its device code and scans from the terminal. A research group that keeps circuits and calibration notebooks on an internal network can run the scan on a machine inside that network.

Click through it at your own pace, or open it full screen.

This demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. A team can then ask in plain language which of its projects carry a given version of a package such as numpy or scipy.

Click through it at your own pace, or open it full screen.

FAQ

What does it mean to transpile a quantum circuit?

IBM's documentation defines transpilation as rewriting a circuit to match the topology of a specific device and optimizing it for execution on noisy hardware. In practice that means choosing a layout, inserting SWAPs, translating to native gates and cancelling redundant gates.

Is OpenQASM 3 backward compatible with OpenQASM 2?

Mostly, but not as a strict superset. OpenQASM 3 keeps the gate and register ideas and still accepts the qreg keyword for compatibility, but it prefers qubit[n] declarations and changes the built-in gates (U and gphase) and the classical syntax. Tools such as Qiskit keep separate importers and exporters for each version, and the repository keeps 2.0 on its own branch.

Why are SWAP gates so expensive?

On hardware without a native SWAP, one SWAP costs three CNOTs, each of which is a noisy two qubit operation. IBM's transpiler guide calls minimizing SWAPs a primary goal of transpilation for that reason.

What is the SABRE algorithm?

SABRE is a SWAP-based bidirectional heuristic search for qubit mapping, published by Li, Ding and Xie at ASPLOS 2019. It scores candidate SWAPs on the gates that are blocked, uses a forward and reverse pass to pick a good starting layout, and adds a decay term to spread SWAPs out. Qiskit's SabreLayout and SabreSwap passes are based on it.

Why does my transpiled circuit change between runs?

Because layout and routing use randomized heuristics. Set seed_transpiler in Qiskit to get the same output each time; in our test the same seed produced an identical circuit.

Do trapped ion computers need qubit routing?

Within one chain, IonQ's docs say the MS gate can entangle any two ions, so SWAPs for connectivity are not needed. Translation to native gates still happens, and larger systems may have their own constraints described in the provider's docs.

What is the difference between OpenQASM and QIR?

OpenQASM is a human readable language for circuits, timing and pulses. QIR is a set of conventions on LLVM IR for compilers, with profiles that say how much classical control a target supports. A toolchain can use both: a front end may emit QIR for one back end and OpenQASM for another.

How do I make a quantum experiment reproducible?

Pin the SDK and its plugins with hashes, record the transpiler seed and optimization level, save the transpiled circuit (OpenQASM 3 export works well for this) and the backend calibration snapshot you ran against, and keep an SBOM of the environment.

Quick check

A 3 qubit fan-out GHZ circuit is compiled onto a line 0, 1, 2 with the trivial layout. How many SWAPs are inserted, and how many two qubit gates does the output have in a CX basis?

One SWAP, because virtual qubit 0 must interact with qubit 2, which is not its neighbour. The SWAP becomes three CNOTs, so the output has 5 two qubit gates instead of 2. Placing virtual qubit 0 on the middle physical qubit removes the SWAP entirely.

Two runs of the same circuit report depth 9 and depth 5. Can you conclude the depth 5 run used a better compiler?

No. Check that both used the same native gate set, coupling map, optimization level and seed. In our example the same circuit had depth 5 in a CX basis and 9 in a CZ basis at level 3, with identical two qubit gate counts.

Sources

Last reviewed: 25 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.