Education / Quantum Programming

Quantum Programming · Chapter 10

Compilation and Transpilation

How does a logical program become executable on a specific device?

In this chapter · 3 sections
  1. 10.1 Backend Targets
  2. 10.2 Transpilation
  3. 10.3 Compiler Inspection
Reading guide · preparation and approach

Starting point: Earlier programming chapters; relevant constructions from Quantum Computing and Quantum Algorithms.

Anchor example: Compile one circuit onto different connectivity constraints and compare costs.

This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.

Our tests compare a program with its logical specification: H here, a CNOT there, on wires we numbered ourselves. A device offers something else. It has its own small set of operations, and each of its qubits can interact only with certain others. Earlier we named the compiler’s job, rewriting a circuit while keeping its meaning, without seeing it done.

How does a logical program become executable on a specific device? We use one small circuit throughout: H on qubit 0, then a CNOT from qubit 0 to qubit 2. We first describe what a device offers, its native operations and its connections. On a line of three qubits, qubits 0 and 2 are not neighbours, so the CNOT forces the compiler to move states around, and we work out what that costs. We then open the compiler itself, see which choices it made, and check that the rewritten circuit still means the original one.

The same circuit, compiled for two different connection patterns, shows where the extra cost comes from.

10.1 Backend Targets

Our circuit asks for a CNOT between qubits 0 and 2, but the device has only a line of qubits in which 0 touches 1 and 1 touches 2. Before the compiler can do anything, it needs a precise description of the device. What must that description contain? We consider the operations first, then the connections between qubits.

10.1.1 Native operations

Which gates can the device actually perform? In Qiskit the device is called a backend, and its description is called its target. The target lists the native operations together with the qubits on which each one is available. It often adds how long each operation takes and how accurately it was last calibrated.

Is a list of gate names enough? No, because an operation may exist on some qubits or qubit pairs and not on others. That is exactly the constraint our CNOT runs into. To see the problem without contacting a device, the companion builds a toy target by hand. Its native gates are rz, sx, x and cx, where rz is a Z rotation and sx is \(\sqrt X\), a quarter turn about X. Its connections form a line of three qubits.

10.1.2 Connectivity and device constraints

Before asking how to run our CNOT, we must say which qubit is meant. The qubits of our program are logical qubits: variables of the algorithm. The device’s qubits are physical qubits: places where the computation happens. A compiler chooses which physical qubit holds each logical one, and that correspondence can change as the circuit runs.

Now suppose logical qubits 0, 1 and 2 sit on physical qubits 0, 1 and 2 of the line. The CNOT from 0 to 2 cannot be applied directly. One way out is to swap the states of physical qubits 1 and 2. Logical qubit 2 then sits next to logical qubit 0, and the CNOT can act on physical qubits 0 and 1. A swap is three CNOTs, so this one logical CNOT becomes four. Some devices also allow a two-qubit gate in only one direction, and reversing it costs extra one-qubit gates. These costs depend on the circuit and on where we place it.

Problem 10.1. Why may a native gate count exceed a logical count?

Worked solution

A logical gate may require decomposition or routing. The two counts use different operation vocabularies.

10.2 Transpilation

We now know what the device offers and what our circuit asks for. Rewriting one into the other is called transpilation. It has to solve two problems in our example: H is not a native gate, and the CNOT acts on qubits that are not neighbours. We take the gate problem first, then the placement problem.

10.2.1 Decomposition and synthesis

How does the compiler run H on a device whose only one-qubit gates are rz, sx and x? It rewrites H in those gates. The compiled circuit contains \(R_z(\pi/2)\,\sqrt X\,R_z(\pi/2)\), which equals H up to a global phase of \(\pi/4\), and Qiskit records that phase with the circuit. Breaking an operation into a known sequence of smaller ones is called decomposition.

Some operations have no exact short decomposition in the available gates. For these, the compiler constructs a sequence that approximates the operation within a stated error. That is called synthesis. Transpilation combines decomposition and synthesis with placement, routing, and simplification. Its optimization level, from 0 to 3, selects how much effort these steps use. A higher level tries harder, but it does not guarantee a smaller or better circuit for every input.

10.2.2 Layout, routing, and optimization

Now the CNOT. The compiler first fixes an initial layout, which places each logical qubit on a physical one. The companion fixes it to the identity, logical \(k\) on physical \(k\). Then routing inserts swaps wherever a two-qubit gate acts on non-neighbours. The compiled circuit for the line is exactly the plan we worked out by hand:

global phase: π/4
         ┌─────────┐┌────┐┌─────────┐
q_0 -> 0 ┤ Rz(π/2) ├┤ √X ├┤ Rz(π/2) ├──■──
         └─────────┘├───┬┘└─────────┘┌─┴─┐
q_1 -> 1 ─────■─────┤ X ├──────■─────┤ X ├
            ┌─┴─┐   └─┬─┘    ┌─┴─┐   └───┘
q_2 -> 2 ───┤ X ├─────■──────┤ X ├────────
            └───┘            └───┘

Three CNOTs swap physical qubits 1 and 2, and the fourth acts on physical 0 and 1. The swap is never undone, so the circuit ends with logical qubit 1 on physical qubit 2 and logical qubit 2 on physical qubit 1. This leftover rearrangement is the final layout, which the companion prints as [0, 2, 1].

The final layout has a consequence for testing. If we compared the compiled circuit’s matrix directly with the original, the swapped wires would make a correct compilation look wrong. Operator.from_circuit reads the layout recorded with the compiled circuit and undoes it, so the comparison is between the same logical qubits.

Problem 10.2. What does an optimization level guarantee?

Worked solution

It selects a compilation strategy; it does not certify a globally minimal circuit or better physical performance on every backend.

10.3 Compiler Inspection

The compiler made choices: a layout, a route, a decomposition. Can we control those choices, reproduce them and report what they cost? We first look at the object that carries out the compilation, then check its output for meaning and count its resources.

10.3.1 Pass managers

What actually runs when we compile? Transpilation is a sequence of steps called passes. Some passes analyse the circuit, for example by finding which gates act on non-neighbours. Others transform it, by choosing a layout, inserting swaps, or decomposing gates. An object that holds the passes and runs them in order is a pass manager.

Qiskit’s convenience function transpile builds a standard pass manager for us and runs it. The companion builds that pass manager explicitly with generate_preset_pass_manager, giving it the optimization level, the native gates, the connections, the initial layout and a seed. Then pass_manager.run(qc) compiles the circuit. Some passes make random choices, and fixing seed_transpiler makes those choices reproducible within the tested implementation. Running several seeds and keeping the best result is a legitimate search, but its classical cost belongs in the report.

The companion wraps this in a function, compile_for(coupling_map), so that it can compile the same circuit for two connection patterns. CouplingMap.from_line(3) is our line. CouplingMap.from_full(3) connects every pair of qubits.

10.3.2 Logical equivalence and physical resource counts

Does the compiled circuit still mean the original? For each connection pattern, the companion checks that Operator.from_circuit(compiled) is equivalent to the logical operator. Where does the extra cost come from? On the full pattern, the CNOT needs no routing and the count stays at one. On the line, it becomes four, as we predicted. The single-qubit gates are the same in both, so the difference is entirely the cost of connectivity.

"""Runnable companion for Quantum Programming, Chapter 10."""
from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap, generate_preset_pass_manager
from qiskit.quantum_info import Operator

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 2)

def compile_for(coupling_map):
    # The same preset pipeline that transpile() builds internally.
    pass_manager = generate_preset_pass_manager(
        optimization_level=1, basis_gates=["rz", "sx", "x", "cx"],
        coupling_map=coupling_map, initial_layout=[0, 1, 2], seed_transpiler=19)
    return pass_manager.run(qc)

line = compile_for(CouplingMap.from_line(3))
full = compile_for(CouplingMap.from_full(3))
for compiled in (line, full):
    assert Operator.from_circuit(compiled).equiv(Operator(qc))
assert full.count_ops()["cx"] == 1 and line.count_ops()["cx"] > 1
print("Logical", qc.count_ops())
print("Line", line.count_ops(), "full", full.count_ops())
print("Line final layout:", line.layout.final_index_layout())

Companion script: chapter_10.py.

For larger circuits, the full matrix comparison is impossible, by the \(4^n\) count we derived for operators. Then we rely on tests of each component, structured tests on chosen inputs, and checks of observables, each stated with its limits. A resource report should also include any added ancillas, the routing, the precision of any synthesis, and the target it assumes.

Problem 10.3. Why record the layout with an experiment?

Worked solution

It tells which physical wires realize the logical ones and is needed to interpret observables and sometimes output mappings correctly.

Our compiled circuit is ready for a device, but so far the device was a description we made up. Running on real hardware needs an account, a choice of backend, and a budget. How do we run a tested program there and interpret what comes back?

Sources and further study: IBM, transpiler API.

Summary

Questions and worked problems

These exercises draw on the whole chapter. Attempt each question before opening a hint, explain your reasoning, then compare it with the worked solution.

1. Quick check §

The companion compiles H on qubit 0 and CX from 0 to 2 for a line of three qubits. How many CNOTs does the compiled circuit contain, and where do the extra ones come from?

Hint 1

Qubits 0 and 2 are not neighbours on the line.

Hint 2

A SWAP costs three CNOTs.

Worked solution

The compiled circuit has \(4\) CNOTs: three implement a SWAP that brings the logical qubits together, and one is the requested CNOT. The logical circuit had one. Connectivity, not the algorithm, accounts for three quarters of the entangling gates here.

from qiskit import QuantumCircuit, transpile
from qiskit.transpiler import CouplingMap

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 2)
compiled = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"], coupling_map=CouplingMap.from_line(3),
                     initial_layout=[0, 1, 2], optimization_level=1, seed_transpiler=19)
assert compiled.count_ops()["cx"] == 4

Revisit the relevant explanation

2. Quick check §

A circuit applies H twice to qubit 0 and then CX from 0 to 1. Compare the compiled gate counts at optimization levels 0 and 1. What does the comparison show, and what does an optimization level not guarantee?

Hint 1

Two H gates cancel.

Hint 2

Higher levels try more transformations; they do not promise an optimum.

Worked solution

Level 0 keeps both Hadamards (decomposed into several rz and sx gates) plus the CNOT; level 1 cancels them and leaves just the CNOT. Higher levels apply more passes, which usually reduces counts, but no level guarantees the smallest possible circuit, or even a smaller one than a lower level, for every input. Compare results and report the level used.

from qiskit import QuantumCircuit, transpile

circuit = QuantumCircuit(2)
circuit.h(0)
circuit.h(0)
circuit.cx(0, 1)
basis = ["rz", "sx", "x", "cx"]
level0 = transpile(circuit, basis_gates=basis, optimization_level=0)
level1 = transpile(circuit, basis_gates=basis, optimization_level=1)
assert level0.size() > level1.size() and dict(level1.count_ops()) == {"cx": 1}

Revisit the relevant explanation

3. Quick check §

A device supports CNOT only from qubit 0 to qubit 1. Show that Hadamards on both qubits before and after a CX(0, 1) implement CX(1, 0). What does a wrong-direction CNOT cost?

Hint 1

\(H\) exchanges the roles of \(X\) and \(Z\).

Hint 2

Count the extra single-qubit gates.

Worked solution

\((H\otimes H)\,\mathrm{CX}_{0\to1}\,(H\otimes H)=\mathrm{CX}_{1\to0}\), which Operator equivalence confirms. A reversed CNOT therefore costs four extra Hadamards (or their native equivalents). Direction constraints, like connectivity, are part of the target and appear in compiled counts and depth.

from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator

flipped = QuantumCircuit(2)
flipped.h([0, 1])
flipped.cx(0, 1)
flipped.h([0, 1])
reverse = QuantumCircuit(2)
reverse.cx(1, 0)
assert Operator(flipped).equiv(Operator(reverse))

Revisit the relevant explanation

4. Worked problem §

For the companion’s compiled circuit, (a) compare Operator(compiled) directly with Operator(circuit); (b) compare Operator.from_circuit(compiled) with Operator(circuit); (c) inspect the final layout. Explain the results.

Hint 1

Routing can leave logical qubits at different physical positions.

Hint 2

from_circuit uses the layout stored on the compiled circuit.

Worked solution

(a) The direct comparison fails. (b) The layout-aware comparison succeeds. (c) The final index layout is \([0,2,1]\): after routing, logical qubits 1 and 2 have exchanged physical positions, because the inserted SWAP was not undone. A matrix built on physical indices therefore differs from the logical operator by that permutation. Equivalence checks of compiled circuits must account for layout, or they report false errors (or, worse, hide real ones).

from qiskit import QuantumCircuit, transpile
from qiskit.transpiler import CouplingMap
from qiskit.quantum_info import Operator

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 2)
compiled = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"], coupling_map=CouplingMap.from_line(3),
                     initial_layout=[0, 1, 2], optimization_level=1, seed_transpiler=19)
assert not Operator(compiled).equiv(Operator(circuit))
assert Operator.from_circuit(compiled).equiv(Operator(circuit))
assert compiled.layout.final_index_layout() == [0, 2, 1]

Revisit the relevant explanation

5. Worked problem §

Recompile the companion with initial_layout=[0, 2, 1], placing logical qubit 2 on physical qubit 1. Predict the CNOT count, and explain why layout is part of a resource estimate.

Hint 1

Logical qubits 0 and 2 now sit on neighbouring physical qubits.

Hint 2

No SWAP is needed.

Worked solution

With logical qubits 0 and 2 placed on adjacent physical qubits 0 and 1, the CNOT is native and no routing is needed: \(1\) CNOT instead of \(4\). The same logical circuit can cost four times more depending on placement, so a reported count must state the target, the layout (or the layout method), and the seed.

from qiskit import QuantumCircuit, transpile
from qiskit.transpiler import CouplingMap

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 2)
compiled = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"], coupling_map=CouplingMap.from_line(3),
                     initial_layout=[0, 2, 1], optimization_level=1, seed_transpiler=19)
assert compiled.count_ops()["cx"] == 1

Revisit the relevant explanation

6. Worked problem §

Compile an arbitrary single-qubit unitary (a seeded random_unitary wrapped in UnitaryGate) to the basis rz, sx, x. How many gates result? Why can every single-qubit unitary be written with at most two SX gates and three RZ gates?

Hint 1

Any single-qubit unitary is a product of Z and Y rotations (Euler angles).

Hint 2

A Y rotation can be built from SX and RZ.

Worked solution

The compiled circuit has \(3\) RZ and \(2\) SX gates. Every single-qubit unitary has an Euler decomposition \(R_z(\alpha)R_y(\beta)R_z(\gamma)\) up to global phase, and a Y rotation can be written as RZ rotations sandwiching SX gates, giving the pattern RZ, SX, RZ, SX, RZ. Arbitrary single-qubit gates are cheap on this basis; two-qubit gates and routing dominate compiled costs.

from qiskit import QuantumCircuit, transpile
from qiskit.circuit.library import UnitaryGate
from qiskit.quantum_info import Operator, random_unitary

unitary = random_unitary(2, seed=4)
circuit = QuantumCircuit(1)
circuit.append(UnitaryGate(unitary), [0])
compiled = transpile(circuit, basis_gates=["rz", "sx", "x"], optimization_level=1)
assert dict(compiled.count_ops()) == {"rz": 3, "sx": 2}
assert Operator(compiled).equiv(unitary)

Revisit the relevant explanation

7. Worked problem §

A four-qubit circuit applies a CNOT between every pair of qubits (six CNOTs). Compile it for all-to-all connectivity and for a line. Compare the CNOT counts, and estimate the failure contribution of the extra gates if each CNOT fails with probability \(5\times10^{-3}\).

Hint 1

On a line, only neighbouring pairs are native.

Hint 2

Extra failure \(\approx\) extra CNOTs \(\times5\times10^{-3}\).

Worked solution

All-to-all: \(6\) CNOTs. Line: about \(11\) in this run, nearly double, because non-neighbouring pairs need routing. The extra five CNOTs add roughly \(5\times5\times10^{-3}=2.5\%\) failure probability. Interaction patterns that are dense in qubit pairs pay heavily on sparse hardware unless the algorithm is restructured, for example with the swap networks of Quantum Hardware Chapter 17.

from qiskit import QuantumCircuit, transpile
from qiskit.transpiler import CouplingMap

circuit = QuantumCircuit(4)
for a in range(4):
    for b in range(a + 1, 4):
        circuit.cx(a, b)
basis = ["rz", "sx", "x", "cx"]
full = transpile(circuit, basis_gates=basis, optimization_level=1)
line = transpile(circuit, basis_gates=basis, coupling_map=CouplingMap.from_line(4), optimization_level=1, seed_transpiler=1)
assert full.count_ops()["cx"] == 6 and line.count_ops()["cx"] > 6

Revisit the relevant explanation

8. Misconception §

A student compiles a circuit for one device and then runs the compiled circuit on a different device “because it is already in native gates.” Name three reasons the compiled circuit may be invalid or poor on the second device.

Hint 1

Compilation used the first device’s target.

Hint 2

Consider gate set, connectivity, and calibration data.

Worked solution

The second device may support a different native gate set (for example a different entangling gate), have different connectivity or allowed CNOT directions, so the routed circuit uses pairs it cannot couple, and have different error rates, so a layout chosen to avoid bad qubits on the first device may land on bad qubits on the second. A compiled circuit is specific to its target; keep the logical circuit and recompile for each backend.

Revisit the relevant explanation

9. Extension §

Full Operator equivalence checks are impossible for large compiled circuits. Propose a scalable check and implement a small version: compare the compiled and logical circuits on several random product input states, accounting for the layout. What can such a check miss?

Hint 1

Prepare each qubit in a random single-qubit state; compare output statevectors.

Hint 2

Use Statevector on the compiled circuit with its layout permutation, or compare Operator.from_circuit on small cases.

Worked solution

For each trial, prepare a random product state on the logical qubits, run it through the logical circuit, and through the compiled circuit with inputs and outputs permuted by the layout; then compare. Statevector simulation scales further than dense matrices, and product inputs are easy to prepare. The small implementation below checks the companion’s circuit this way. Random tests can miss errors confined to a small subspace, so for critical components they should complement structured proofs rather than replace them.

import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit.transpiler import CouplingMap
from qiskit.quantum_info import Operator, Statevector, random_unitary

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 2)
compiled = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"], coupling_map=CouplingMap.from_line(3),
                     initial_layout=[0, 1, 2], optimization_level=1, seed_transpiler=19)
logical = Operator.from_circuit(compiled)
for seed in range(5):
    prep = QuantumCircuit(3)
    for q in range(3):
        prep.unitary(random_unitary(2, seed=10 * seed + q), [q])
    expected = Statevector.from_instruction(prep.compose(circuit))
    assert Statevector.from_instruction(prep).evolve(logical).equiv(expected)

Revisit the relevant explanation

10. Extension §

Explain what seed_transpiler controls, and design a fair comparison between two compilation strategies that both use randomized passes. What should be reported alongside the best circuit found?

Hint 1

Layout and routing passes may make random choices.

Hint 2

Searching over seeds is itself a classical cost.

Worked solution

seed_transpiler fixes the pseudorandom choices inside randomized passes, such as trial layouts and routing decisions, making a compilation reproducible with the same software version. A fair comparison runs both strategies over the same set of seeds (or the same number of trials) on the same target, and reports the distribution of results (mean, spread, best), the number of seeds tried, the compilation time, and the software versions. Reporting only the best of many seeds for one strategy and a single seed for the other overstates the difference.

Revisit the relevant explanation

Search this book

Search the course.