skills/qiskit/references/transpilation.md
Transpilation rewrites an abstract circuit into an instruction set architecture (ISA) circuit that satisfies a specific backend Target:
IBM Runtime V2 primitives require ISA circuits. They do not perform layout, routing, and basis translation automatically.
Use a preset staged pass manager:
from qiskit.transpiler import generate_preset_pass_manager
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=17,
)
isa_circuit = pass_manager.run(circuit)
The pass manager can be reused for circuits targeting the same backend snapshot:
isa_circuits = pass_manager.run(circuits)
qiskit.transpile() remains a convenient wrapper, but a pass manager is easier to reuse, inspect, and customize.
backend.target is the source of supported operations and constraints:
print("backend:", backend.name)
print("qubits:", backend.num_qubits)
print("operations:", sorted(backend.operation_names))
print("coupling map:", backend.coupling_map)
print("target:", backend.target)
Do not use these removed or legacy BackendV1 access patterns:
# Do not use in Qiskit 2.x code:
# backend.configuration().basis_gates
# backend.properties()
# BackendProperties
For a synthetic target:
from qiskit.transpiler import CouplingMap, Target
target = Target.from_configuration(
basis_gates=["cz", "sx", "rz"],
coupling_map=CouplingMap.from_line(5),
)
pass_manager = generate_preset_pass_manager(
target=target,
optimization_level=1,
seed_transpiler=17,
)
isa_circuit = pass_manager.run(circuit)
Prefer one coherent backend or target. Combining a backend with separate basis_gates or coupling_map inputs creates competing sources of truth and is discouraged.
| Level | Typical intent |
|---|---|
| 0 | Minimal transformation needed to satisfy the target |
| 1 | Light optimization with relatively low compilation cost |
| 2 | More optimization and search |
| 3 | Heavier optimization and search; highest classical cost |
Higher does not guarantee a better experimental result. Compare levels using the same circuit, target snapshot, and seed:
compiled = {}
for level in (0, 1, 2, 3):
manager = generate_preset_pass_manager(
backend=backend,
optimization_level=level,
seed_transpiler=17,
)
compiled[level] = manager.run(circuit)
Start with levels 1 and 3 for a bounded comparison. Evaluate target-native two-qubit operations, depth, estimated duration when available, and downstream result quality.
A preset StagedPassManager has six conceptual stages:
Qiskit 2.x scheduling does not restore qiskit.pulse; that module was removed.
Inspect the generated manager:
print(pass_manager)
print(pass_manager.property_set)
The property set is primarily a pass-development and debugging interface; do not build long-term application logic around undocumented keys.
Transpilation can permute logical qubits. Estimator observables must be mapped to the final physical layout:
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("ZZ", 1.0)])
isa_circuit = pass_manager.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
Submit the mapped observable:
pub = (isa_circuit, isa_observable, parameter_values)
Do not manually guess the permutation from a circuit drawing. Use isa_circuit.layout.
For a list of observables:
isa_observables = [
observable.apply_layout(isa_circuit.layout)
for observable in observables
]
Transpile the parameterized circuit once:
parameterized_isa = pass_manager.run(parameterized_circuit)
isa_observable = observable.apply_layout(parameterized_isa.layout)
for values in optimizer_values:
pub = (parameterized_isa, isa_observable, [values])
result = estimator.run([pub], precision=0.03).result()[0]
This avoids repeated layout and routing. Retranspile only when the circuit structure, target, selected backend feature set, or compilation strategy changes.
Use an explicit layout only when calibration analysis or a reproducibility requirement justifies it:
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=2,
initial_layout=[3, 5, 8],
seed_transpiler=17,
)
An explicit layout bypasses the preset layout-selection logic. Confirm every physical qubit and interaction is supported by the target.
approximation_degree trades unitary fidelity for potentially cheaper circuits:
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
approximation_degree=0.99,
seed_transpiler=17,
)
Treat approximation as an experimental parameter. Compare ideal-unitary error, native two-qubit count, and hardware outcome. Do not describe 0.99 as a universal one-percent error bound for the whole algorithm.
Set the transpiler seed explicitly:
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
seed_transpiler=17,
)
Qiskit 2.5 also supports QISKIT_TRANSPILER_SEED, but an explicit argument is clearer in reproducible code.
Record:
A fixed seed stabilizes stochastic compiler choices; it does not freeze changing backend calibration data or package behavior across releases.
def two_qubit_instruction_count(circuit):
return sum(
len(item.qubits) == 2
for item in circuit.data
)
print("logical depth:", circuit.depth())
print("ISA depth:", isa_circuit.depth())
print("ISA size:", isa_circuit.size())
print("ISA operations:", isa_circuit.count_ops())
print("two-qubit instructions:", two_qubit_instruction_count(isa_circuit))
print("layout:", isa_circuit.layout)
Count all two-qubit instructions rather than only cx; current targets may use cz, ecr, rzz, or other operations.
Circuit metrics are proxies. Calibration-aware quality depends on which physical qubits and operations were selected.
Runtime rejects circuits that do not satisfy the target. A practical preflight is:
backend.num_qubits.backend.target.For advanced validation, use target APIs rather than a hardcoded basis list.
Test target-aware compilation without consuming QPU time:
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
fake_backend = FakeSherbrooke()
pass_manager = generate_preset_pass_manager(
backend=fake_backend,
optimization_level=1,
seed_transpiler=17,
)
isa_circuit = pass_manager.run(circuit)
Fake-backend availability can change between Runtime releases. List the installed fake-provider exports rather than assuming an old tutorial backend exists.
For an Aer noise model derived from a real backend:
from qiskit_aer import AerSimulator
noisy_simulator = AerSimulator.from_backend(backend)
simulator_pass_manager = generate_preset_pass_manager(
backend=noisy_simulator,
optimization_level=1,
seed_transpiler=17,
)
simulator_circuit = simulator_pass_manager.run(circuit)
An Aer model is an approximation of the calibration data used to create it; it is not a faithful predictor of every QPU effect.
Backend capabilities can depend on how the backend is requested:
backend = service.backend(
backend_name,
use_fractional_gates=True,
)
Fractional-gate support and dynamic-control support have evolved across Runtime versions. Inspect the returned target and current feature-compatibility documentation instead of assuming both are available for every backend and option combination.
If a circuit uses if_else, while_loop, switch_case, for_loop, or classical expressions:
backend.operation_names,Customize only after measuring a limitation in preset output:
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import RemoveBarriers
cleanup = PassManager([RemoveBarriers()])
cleaned = cleanup.run(circuit)
For target-aware custom pipelines, use StagedPassManager or modify a generated preset stage. Every custom transformation must preserve circuit semantics, parameters, control flow, and global phase as appropriate.
Write regression tests based on operator/state equivalence for small circuits and application-level outputs for larger circuits.
isa_circuit.layout.configuration() and properties() access with BackendV2 target and direct attributes.