Back to Claude Scientific Skills

Target-Aware Transpilation

skills/qiskit/references/transpilation.md

2.55.010.6 KB
Original Source

Target-Aware Transpilation

Transpilation rewrites an abstract circuit into an instruction set architecture (ISA) circuit that satisfies a specific backend Target:

  • only supported instructions,
  • valid qubit operands and connectivity,
  • backend timing and control-flow constraints,
  • an explicit virtual-to-physical qubit layout.

IBM Runtime V2 primitives require ISA circuits. They do not perform layout, routing, and basis translation automatically.

Recommended Entry Point

Use a preset staged pass manager:

python
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:

python
isa_circuits = pass_manager.run(circuits)

qiskit.transpile() remains a convenient wrapper, but a pass manager is easier to reuse, inspect, and customize.

The BackendV2 Target

backend.target is the source of supported operations and constraints:

python
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:

python
# Do not use in Qiskit 2.x code:
# backend.configuration().basis_gates
# backend.properties()
# BackendProperties

For a synthetic target:

python
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.

Preset Optimization Levels

LevelTypical intent
0Minimal transformation needed to satisfy the target
1Light optimization with relatively low compilation cost
2More optimization and search
3Heavier optimization and search; highest classical cost

Higher does not guarantee a better experimental result. Compare levels using the same circuit, target snapshot, and seed:

python
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.

Transpiler Stages

A preset StagedPassManager has six conceptual stages:

  1. init: validate and synthesize high-level operations.
  2. layout: map virtual qubits to physical qubits.
  3. routing: add routing operations to satisfy connectivity.
  4. translation: convert to target-supported instructions.
  5. optimization: simplify and resynthesize the target circuit.
  6. scheduling: apply hardware-aware timing passes when configured.

Qiskit 2.x scheduling does not restore qiskit.pulse; that module was removed.

Inspect the generated manager:

python
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.

Layouts and Observables

Transpilation can permute logical qubits. Estimator observables must be mapped to the final physical layout:

python
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:

python
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:

python
isa_observables = [
    observable.apply_layout(isa_circuit.layout)
    for observable in observables
]

Parameterized Workloads

Transpile the parameterized circuit once:

python
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.

Initial Layouts

Use an explicit layout only when calibration analysis or a reproducibility requirement justifies it:

python
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.

Approximate Synthesis

approximation_degree trades unitary fidelity for potentially cheaper circuits:

python
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.

Reproducibility

Set the transpiler seed explicitly:

python
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:

  • Qiskit version,
  • backend name and target/calibration timestamp if available,
  • optimization level,
  • seed,
  • all non-default pass-manager arguments,
  • input and output QPY artifacts,
  • output layout and structural metrics.

A fixed seed stabilizes stochastic compiler choices; it does not freeze changing backend calibration data or package behavior across releases.

Analyze Compiled Circuits

python
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.

Verify ISA Compatibility

Runtime rejects circuits that do not satisfy the target. A practical preflight is:

  1. Compile using the exact backend.
  2. Confirm the compiled width fits backend.num_qubits.
  3. Inspect every instruction name and qubit tuple against backend.target.
  4. Submit only the pass-manager output.

For advanced validation, use target APIs rather than a hardcoded basis list.

Fake Backends and Aer

Test target-aware compilation without consuming QPU time:

python
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:

python
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.

Dynamic Circuits and Fractional Gates

Backend capabilities can depend on how the backend is requested:

python
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:

  • confirm the operations are present in backend.operation_names,
  • compile against that returned backend object,
  • check mitigation and fractional-gate compatibility,
  • test the full branch behavior.

Custom Pass Managers

Customize only after measuring a limitation in preset output:

python
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.

Common Failures

  • Circuit not ISA-compatible: submit the pass-manager output, not the abstract input.
  • Wrong expectation value: map the observable through isa_circuit.layout.
  • Too many routing operations: compare layout methods, seeds, initial layouts, and circuit connectivity.
  • Compilation is slow: lower the optimization level or reduce repeated transpilation.
  • BackendV1 attribute error: replace configuration() and properties() access with BackendV2 target and direct attributes.
  • Unknown instruction: inspect the exact backend target; do not use a generic basis copied from another device.
  • Dynamic-circuit rejection: verify target control-flow operations and incompatible backend feature flags.
  • Non-reproducible comparison: fix the seed and backend snapshot, and store all compiler settings.