Skip to content

Circuit Transpilation ​

Learn how to convert abstract quantum circuits into hardware-executable form using pyqpanda3's transpilation module. This tutorial covers chip topology modeling, circuit routing, gate decomposition, and the full transpilation pipeline.

Prerequisites: Circuit Construction -- you should be comfortable building QProg and QCircuit objects before working through this tutorial.


Table of Contents ​


Why Transpilation Matters ​

When you write a quantum circuit in pyqpanda3, you express your computation using an abstract gate set -- any of the 37+ gates provided by the core module. For example, you can use CNOT(0, 3) to apply a controlled-NOT between qubits 0 and 3, or SWAP(1, 4) to exchange the states of qubits 1 and 4.

However, real quantum hardware imposes three fundamental constraints:

1. Native gate set. A quantum processor does not implement every gate directly. Most superconducting chips support only a small set of basis gates, such as {U3, CZ} or {RZ, X1, CNOT}. A gate like SWAP or TOFFOLI must be decomposed into a sequence of native gates before execution.

2. Qubit connectivity. Physical qubits are arranged in a specific topology. A two-qubit gate can only be applied between qubits that are physically adjacent. If your circuit contains CNOT(0, 3) but the hardware only connects qubits 0-1, 1-2, and 2-3, the transpiler must insert SWAP operations to route the logical interaction through the physical connections.

3. Gate fidelity and coherence. Different qubits and gate types have different error rates. A good transpiler places logical qubits on the physical qubits with the highest fidelity and minimizes the total number of two-qubit gates.

Without transpilation, a circuit written for an idealized quantum computer cannot run on real hardware. The transpilation module bridges this gap by transforming your abstract circuit into one that satisfies the hardware's constraints while preserving its logical behavior.

mermaid
flowchart LR
    subgraph "Abstract Circuit"
        A1["H(0)"]
        A2["CNOT(0,3)"]
        A3["T(1)"]
        A4["SWAP(0,2)"]
    end

    subgraph "Transpilation"
        T["Transpiler"]
    end

    subgraph "Hardware Circuit"
        B1["RZ(0, pi/2)"]
        B2["X1(0)"]
        B3["CNOT(0,1)"]
        B4["CNOT(1,2)"]
        B5["CNOT(2,3)"]
        B6["CNOT(1,2)"]
        B7["CNOT(0,1)"]
        B8["RZ(1, pi/4)"]
    end

    A1 --> T
    A2 --> T
    A3 --> T
    A4 --> T
    T --> B1
    T --> B2
    T --> B3
    T --> B4
    T --> B5
    T --> B6
    T --> B7
    T --> B8

    style T fill:#50c878,color:#fff,stroke:#2d8a4e

The Transpilation Pipeline ​

Transpilation in pyqpanda3 consists of several stages, each addressing one or more hardware constraints:

mermaid
flowchart TD
    INPUT["Input QProg / QCircuit"]

    subgraph "Stage 1: Gate Decomposition"
        D1["Non-native gates<br/>decomposed into<br/>basic gate set"]
    end

    subgraph "Stage 2: Topology Routing"
        D2["Qubit mapping<br/>and SWAP insertion<br/>using SABRE algorithm"]
    end

    subgraph "Stage 3: Optimization"
        D3["Gate cancellation<br/>and circuit<br/>simplification"]
    end

    OUTPUT["Output QProg"]

    INPUT --> D1
    D1 --> D2
    D2 --> D3
    D3 --> OUTPUT

    style INPUT fill:#4a90d9,color:#fff
    style OUTPUT fill:#50b86c,color:#fff
    style D1 fill:#f4a460,color:#fff
    style D2 fill:#f4a460,color:#fff
    style D3 fill:#f4a460,color:#fff

Gate decomposition rewrites gates that are not in the target hardware's native gate set. For example, a TOFFOLI gate might be decomposed into six CNOT gates plus several single-qubit rotations.

Topology routing maps logical qubits to physical qubits and inserts SWAP gates where needed. pyqpanda3 uses the SABRE (SWAP-based BidiREctional heuristic search) algorithm, which is efficient and produces high-quality mappings.

Optimization applies local rewriting rules to cancel redundant gate pairs (e.g., H followed by H becomes identity) and merge adjacent single-qubit rotations.

The three functions exported by the transpilation module correspond to different parts of this pipeline:

FunctionPurpose
generate_topology(n, type)Generate a synthetic chip topology for testing
Transpiler().transpile(...)Full pipeline: decomposition, routing, and optimization
decompose(...)Standalone gate decomposition without routing

1. Chip Topology ​

1.1 What Is Chip Topology? ​

A chip topology describes which pairs of physical qubits can directly execute two-qubit gates. It is represented as an edge list: a list of pairs [qubit_a, qubit_b] indicating that qubits a and b are physically connected.

For example, a 4-qubit linear chain has the topology:

[0, 1], [1, 2], [2, 3]

Visually:

q0 --- q1 --- q2 --- q3

A 4-qubit square (grid) topology adds diagonal connections:

q0 --- q1
 |      |
q2 --- q3

The topology determines the routing problem. In a linear chain, any two-qubit gate between non-adjacent qubits requires SWAP gates to bring the logical states together. In a fully connected topology, no routing is needed because every qubit pair is directly connected.

1.2 generate_topology() ​

pyqpanda3 provides generate_topology() to create synthetic topology edge lists for testing and experimentation. You do not need to manually construct edge lists for common topologies.

python
from pyqpanda3.transpilation import generate_topology

# Generate a linear chain topology for 4 qubits
topo = generate_topology(4, "linear")
print(topo)
# [[0, 1], [1, 2], [2, 3]]

Output:

[[0, 1], [1, 2], [2, 3]]

Signature:

python
generate_topology(num_qubits: int, topology_type: str) -> list

Parameters:

ParameterTypeDescription
num_qubitsintNumber of physical qubits in the topology
topology_typestrThe topology layout type (see below)

Returns: A list of [qubit_a, qubit_b] pairs representing the physical connections.

1.3 Topology Types ​

The following topology types are supported:

TypeDescriptionBest For
"linear"Chain: q0-q1-q2-...-q(n-1)Simple test circuits
"square"2D grid arrangementGrid-based QPUs
"all-to-all"Every qubit connected to every otherIdealized testing

Linear Topology ​

A linear chain connects each qubit to its immediate neighbors:

python
from pyqpanda3.transpilation import generate_topology

topo = generate_topology(5, "linear")
print(topo)
# [[0, 1], [1, 2], [2, 3], [3, 4]]

Output:

[[0, 1], [1, 2], [2, 3], [3, 4]]
mermaid
graph LR
    Q0["Q0"] --- Q1["Q1"] --- Q2["Q2"] --- Q3["Q3"] --- Q4["Q4"]

    style Q0 fill:#4a90d9,color:#fff
    style Q1 fill:#4a90d9,color:#fff
    style Q2 fill:#4a90d9,color:#fff
    style Q3 fill:#4a90d9,color:#fff
    style Q4 fill:#4a90d9,color:#fff

Square (Grid) Topology ​

A square topology arranges qubits in a 2D grid. Each qubit is connected to its neighbors horizontally and vertically:

python
from pyqpanda3.transpilation import generate_topology

topo = generate_topology(4, "square")
print(topo)
# [[0,1], [0,3], [1,2], [1,4], [2,5],
#  [3,4], [3,6], [4,5], [4,7], [5,8],
#  [6,7], [7,8]]

Output:

[[0, 1], [0, 3], [1, 2], [1, 4], [2, 5], [3, 4], [3, 6], [4, 5], [4, 7], [5, 8], [6, 7], [7, 8]]

Square topologies model many real superconducting chips, where qubits are fabricated on a planar substrate with nearest-neighbor couplings. For 4 qubits, generate_topology creates a 3x3 grid (9 qubits, 12 edges).

All-to-All Topology ​

An all-to-all topology links every qubit to every other qubit:

python
from pyqpanda3.transpilation import generate_topology

topo = generate_topology(4, "all-to-all")
print(topo)
# [[0,1], [0,2], [0,3], [1,2], [1,3], [2,3]]

Output:

[[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]

With all-to-all connectivity, no SWAP gates are needed for routing. This topology is useful for testing decomposition in isolation or simulating idealized hardware. Real ion-trap quantum computers often have all-to-all connectivity.

1.4 Custom Topologies ​

You can also pass custom edge lists directly. For example, a star graph with qubit 0 at the center:

python
custom_topo = [[0, 1], [0, 2], [0, 3], [0, 4]]

Or a ring (circular) topology:

python
ring_topo = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]]

2. The Transpiler Class ​

The Transpiler class is the main entry point for circuit transpilation. It combines gate decomposition, qubit routing, and circuit optimization into a single operation.

2.1 Creating a Transpiler ​

Create a Transpiler instance with no arguments:

python
from pyqpanda3.transpilation import Transpiler

transpiler = Transpiler()

The transpiler is stateless -- it holds no persistent configuration between calls. All parameters are provided at transpile time.

2.2 transpile() with Edge List ​

The most common way to use the transpiler is to provide an edge list describing the chip topology:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology

# Build a circuit that requires routing
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 3)       # qubits 0 and 3 are not adjacent in a 4-qubit chain
prog << core.measure([0, 3], [0, 3])

# Define the chip topology as an edge list
chip_edges = [[0, 1], [1, 2], [2, 3]]

# Transpile: route through the linear chain and decompose to basic gates
transpiler = Transpiler()
transpiled = transpiler.transpile(
    prog,
    chip_edges,            # topology edge list
    {},                    # initial mapping (empty = automatic)
    2                      # optimization level
)

print(transpiled)

Output:

          ┌─┐         ┌─┐     
q_0:  |0>─┤H├ ───*── ─┤M├──── 
          └─┘ ┌──┴─┐  └╥┘ ┌─┐ 
q_1:  |0>──── ┤CNOT├ ──╫──┤M├ 
              └────┘   ║  └╥┘ 
 c :   / ══════════════╩═══╩═
                        0   3

In this example, the original circuit applies CNOT(0, 3), but qubits 0 and 3 are three hops apart in the linear chain. The SABRE algorithm automatically finds an optimal initial mapping and routing strategy. For example, it might map logical qubit 0 to physical qubit 3, allowing the CNOT to be executed directly without inserting SWAP gates. The final transpiled result depends on the SABRE algorithm's heuristic decisions.

Signature:

python
transpiler.transpile(
    prog,                 # QProg: the circuit to transpile
    chip_topology_edges,  # list of [int, int]: physical connections
    init_mapping,         # dict: initial qubit mapping (empty dict for automatic)
    optimization_level,   # int: 0, 1, or 2
    basic_gates=None      # list of str: target gate set (optional)
) -> QProg

Parameters:

ParameterTypeDescription
progQProgThe quantum program to transpile
chip_topology_edgeslist[list[int]]Edge list of the chip topology
init_mappingdictInitial logical-to-physical qubit mapping. Pass {} for automatic.
optimization_levelintOptimization level: 0 (none), 1 (basic), 2 (aggressive)
basic_gateslist[str] or NoneTarget gate set names. If None, uses a default set.

2.3 transpile() with Backend Object ​

You can use generate_topology() to create a chip topology for development and testing:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology

# Generate a 4-qubit linear topology
chip_edges = generate_topology(4, "linear")
print(f"Chip topology: {chip_edges}")

# Build a circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.SWAP(0, 2)
prog << core.measure([0, 1, 2], [0, 1, 2])

# Transpile using the edge list
transpiler = Transpiler()
transpiled = transpiler.transpile(
    prog,
    chip_edges,
    {},                    # initial mapping
    2                      # optimization level
)
print("Transpile succeeded")

Output:

Chip topology: [[0, 1], [1, 2], [2, 3]]
Transpile succeeded

When targeting real quantum hardware, you would authenticate via the cloud service and obtain the chip topology from the backend. Using generate_topology() is useful for local testing and simulation.

2.4 Optimization Levels ​

The optimization_level parameter controls how aggressively the transpiler optimizes the output circuit. Higher levels produce shorter circuits at the cost of longer transpilation time.

LevelBehaviorUse Case
0No optimization. Only routing and decomposition.Debugging; seeing raw routing output
1Basic optimization. Cancels adjacent inverse gates and merges rotations.Fast transpilation with cleanup
2Aggressive optimization. All rewriting rules including commutativity-based reordering.Production circuits for hardware
python
# Level 0: raw output, no simplification
transpiled = transpiler.transpile(prog, chip_edges, {}, 0)

# Level 1: basic gate cancellation
transpiled = transpiler.transpile(prog, chip_edges, {}, 1)

# Level 2: aggressive optimization (recommended for hardware)
transpiled = transpiler.transpile(prog, chip_edges, {}, 2)

2.5 Initial Qubit Mapping ​

The init_mapping parameter lets you specify which logical qubit should start on which physical qubit. This is a dictionary mapping logical qubit indices to physical qubit indices:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler

# Build a 2-qubit circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# 3-qubit linear chain: physical qubits 0-1-2
chip_edges = [[0, 1], [1, 2]]

# Map logical qubit 0 -> physical qubit 2
# Map logical qubit 1 -> physical qubit 0
init_mapping = {0: 2, 1: 0}

transpiler = Transpiler()
transpiled = transpiler.transpile(prog, chip_edges, init_mapping, 2)
print("Transpile succeeded with custom mapping")

Output:

Transpile succeeded with custom mapping

When you pass an empty dictionary {}, the transpiler uses the SABRE algorithm to find an initial mapping automatically. In most cases, the automatic mapping is sufficient and you do not need to provide one.

You might want to specify a custom mapping when:

  • You know that certain qubits on the hardware have higher fidelity and want to place important logical qubits there.
  • You are running multiple circuits and want them to use the same mapping for result comparison.

2.6 Specifying Basic Gates ​

The basic_gates parameter restricts the output circuit to use only gates from the specified set. Any gate not in this set will be decomposed:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler

# Build a circuit
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

chip_edges = [[0, 1], [1, 2]]
transpiler = Transpiler()

# Target the IBM-like gate set: U3 + CNOT is universal
transpiled = transpiler.transpile(
    prog, chip_edges, {}, 2,
    basic_gates=["U3", "CNOT"]
)
print("Transpiled with U3/CNOT gate set")

Output:

Transpiled with U3/CNOT gate set

Common gate sets:

Gate SetGatesHardware
IBM-like["U3", "CNOT"] or ["RZ", "X1", "CNOT"]Superconducting
OriginQ-like["U3", "CZ"] or ["RZ", "X1", "CZ"]Superconducting
Clifford+T["H", "S", "T", "CNOT"]Theoretical analysis
Ion trap["RXX", "RZ", "RX"] or ["MS", "RZ"]Trapped ion

If basic_gates is not specified (or None), the transpiler uses a default gate set.


3. Gate Decomposition ​

The decompose function performs gate decomposition without routing. It takes a circuit (or matrix) and rewrites all gates in terms of a specified basis gate set. This is useful when you want to see the gate decomposition in isolation, without modifying qubit assignments.

3.1 Why Decompose Gates? ​

Gate decomposition serves several purposes:

  1. Hardware compatibility: Real QPUs implement a fixed set of physical operations. Any gate outside this set must be expressed as a sequence of supported gates.

  2. Circuit analysis: Decomposing into a standard gate set makes it easier to count resources (e.g., how many two-qubit gates are actually needed).

  3. Gate count optimization: A high-level gate like TOFFOLI requires 6 CNOT gates when decomposed. Knowing this helps you estimate circuit cost.

  4. Verification: Decomposing a unitary matrix into gates and checking that the resulting circuit's matrix matches the original verifies correctness.

The decomposition rules follow well-known identities from quantum circuit synthesis. For example:

3.2 decompose(prog, basic_gates) ​

Decompose all gates in a QProg into the specified basic gate set:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import decompose

# Build a circuit with non-native gates
prog = core.QProg()
prog << core.H(0)
prog << core.SWAP(0, 1)        # SWAP is not in most native gate sets
prog << core.TOFFOLI(0, 1, 2)  # TOFFOLI is always decomposed
prog << core.measure([0, 1, 2], [0, 1, 2])

# Decompose into H + CNOT + rotation gates
decomposed = decompose(prog, ["H", "CNOT", "RZ", "X1"])

print(f"Original gate count: {prog.count_ops()}")
print(f"Decomposed gate count: {decomposed.count_ops()}")

Output:

Original gate count: {'CCX': 1, 'SWAP': 1, 'H': 1}
Decomposed gate count: {'RZ': 7, 'CNOT': 9, 'H': 3}

Signature:

python
decompose(prog: QProg, basic_gates: list) -> QProg

Parameters:

ParameterTypeDescription
progQProgInput quantum program
basic_gateslist[str]Target gate set names

Returns: A new QProg with all gates expressed in terms of basic_gates.

The original program is not modified. The decompose function always returns a new program object.

3.3 decompose(circuit, basic_gates) ​

Decompose gates in a QCircuit object:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import decompose

# Build a circuit module
circuit = core.QCircuit()
circuit << core.SWAP(0, 1)
circuit << core.CZ(1, 2)

# Decompose into CNOT + single-qubit gates
decomposed_circuit = decompose(circuit, ["H", "CNOT", "RZ", "X1"])

print(f"Original depth: {circuit.depth()}")
print(f"Decomposed depth: {decomposed_circuit.depth()}")

Output:

Original depth: 2
Decomposed depth: 7

Signature:

python
decompose(circuit: QCircuit, basic_gates: list) -> QCircuit

Parameters:

ParameterTypeDescription
circuitQCircuitInput quantum circuit
basic_gateslist[str]Target gate set names

Returns: A new QCircuit with all gates expressed in terms of basic_gates.

This overload is useful when you are working with reusable circuit modules rather than full programs. The decomposed circuit can then be embedded into any QProg.

3.4 decompose(matrix, qubits) ​

Decompose a unitary matrix into a quantum circuit. This is the most powerful form of decomposition -- it takes an arbitrary 2n×2n unitary matrix and synthesizes a circuit that implements it.

python
import numpy as np
from pyqpanda3.transpilation import decompose

# Define a 2x2 unitary matrix (single-qubit gate)
# This is a phase gate: diag(1, exp(i*pi/4))
mat = np.array([
    [1, 0],
    [0, np.exp(1j * np.pi / 4)]
], dtype=complex)

# Decompose into a circuit acting on qubit 0
circuit = decompose(mat, [0])
print(f"Circuit depth: {circuit.depth()}")
print(f"Circuit size: {circuit.size()}")

Output:

Circuit depth: 1
Circuit size: 1

Signature:

python
decompose(matrix: np.ndarray, qubits: list) -> QCircuit

Parameters:

ParameterTypeDescription
matrixnp.ndarrayUnitary matrix to decompose. Must be 2n×2n for n qubits.
qubitslist[int]Qubit indices the circuit acts on. Length must be n.

Returns: A QCircuit that implements the given unitary on the specified qubits.

Two-qubit example:

python
import numpy as np
from pyqpanda3.transpilation import decompose

# A 4x4 unitary (two-qubit gate)
# Create a controlled-phase gate matrix
cp_mat = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, np.exp(1j * np.pi / 3)]
], dtype=complex)

# Decompose into a circuit on qubits 0 and 1
circuit = decompose(cp_mat, [0, 1])
print(f"2Q circuit depth: {circuit.depth()}")
print(f"2Q gate count: {circuit.count_ops(only_q2=True)}")

Output:

2Q circuit depth: 5
2Q gate count: {'CZ': 2}

Verification:

You can verify that the decomposed circuit correctly implements the original matrix:

python
import numpy as np
from pyqpanda3 import core
from pyqpanda3.transpilation import decompose

# Original matrix
mat = np.array([
    [1, 0],
    [0, np.exp(1j * np.pi / 4)]
], dtype=complex)

# Decompose
circuit = decompose(mat, [0])

# Compute the circuit's unitary
circuit_matrix = circuit.matrix()

# Check that they match
print("Matrices match:", np.allclose(mat, circuit_matrix))
# Matrices match: True

Output:

Matrices match: True

3.5 Decomposition Rules ​

The decomposition engine applies well-known quantum circuit identities. Here are the most important ones:

SWAP decomposition:

SWAP(a,b)=CNOT(a,b)⋅CNOT(b,a)⋅CNOT(a,b)

This requires 3 CNOT gates.

python
from pyqpanda3 import core
from pyqpanda3.transpilation import decompose

swap_prog = core.QProg()
swap_prog << core.SWAP(0, 1)
result = decompose(swap_prog, ["H", "CNOT", "RZ", "X1"])
# result now contains: CNOT(0,1), CNOT(1,0), CNOT(0,1)

CZ decomposition:

CZ(a,b)=(I⊗H)⋅CNOT(a,b)⋅(I⊗H)

TOFFOLI decomposition:

A Toffoli (CCX) gate decomposes into 6 CNOT gates and 7 single-qubit gates:

TOFFOLI(a,b,c)=sequence of 6 CNOTs + T/Tdg + H gates

Single-qubit gate decomposition:

Any single-qubit unitary U can be decomposed into at most 3 rotation gates using the ZYZ decomposition:

U=RZ(α)⋅RY(β)⋅RZ(γ)

Or into a U3 gate:

U=U3(θ,ϕ,λ)

4. Putting It All Together ​

4.1 Full Transpilation Workflow ​

Here is the complete workflow for transpiling a circuit from an abstract representation to a hardware-ready form:

mermaid
flowchart TD
    A["1. Build abstract circuit<br/>using core gates"] --> B["2. Define chip topology<br/>or get from backend"]
    B --> C["3. Choose optimization level<br/>and basic gate set"]
    C --> D["4. Call transpiler.transpile()"]
    D --> E["5. Verify: compare<br/>before and after"]
    E --> F{Results match?}
    F -->|Yes| G["6. Run on simulator<br/>or submit to hardware"]
    F -->|No| H["Debug mapping<br/>or decomposition"]
    H --> C

    style A fill:#4a90d9,color:#fff
    style G fill:#50b86c,color:#fff
    style H fill:#c0392b,color:#fff

Complete example:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology, decompose

# Step 1: Build the abstract circuit
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.SWAP(0, 2)
prog << core.T(1)
prog << core.measure([0, 1, 2], [0, 1, 2])

print("=== Original Circuit ===")
print(f"Depth: {prog.depth(only_q2=True)}")
print(f"Gates: {prog.count_ops()}")
print(f"2Q Gates: {prog.count_ops(only_q2=True)}")

# Step 2: Define chip topology (4-qubit linear chain)
chip_edges = generate_topology(4, "linear")
print(f"\nChip topology: {chip_edges}")

# Step 3: Transpile with optimization level 2
transpiler = Transpiler()
transpiled = transpiler.transpile(
    prog,
    chip_edges,
    {},                           # automatic qubit mapping
    2,                            # aggressive optimization
    basic_gates=["H", "RZ", "X1", "CNOT"]
)

print("\n=== Transpiled Circuit ===")
print(f"Depth: {transpiled.depth(only_q2=True)}")
print(f"Gates: {transpiled.count_ops()}")
print(f"2Q Gates: {transpiled.count_ops(only_q2=True)}")

# Step 4: Verify correctness on a simulator
machine = core.CPUQVM()
machine.run(prog, 10000)
orig_probs = machine.result().get_prob_dict()

machine.run(transpiled, 10000)
transpiled_probs = machine.result().get_prob_dict()

print("\n=== Verification ===")
print(f"Original probabilities:    {orig_probs}")
print(f"Transpiled probabilities:  {transpiled_probs}")

Output:

=== Original Circuit ===
Depth: 2
Gates: {'SWAP': 1, 'CNOT': 1, 'T': 1, 'H': 1}
2Q Gates: {'SWAP': 1, 'CNOT': 1}

Chip topology: [[0, 1], [1, 2], [2, 3]]

=== Transpiled Circuit ===
Depth: 4
Gates: {'RZ': 3, 'X1': 1, 'CNOT': 4}
2Q Gates: {'CNOT': 4}

=== Verification ===
Original probabilities:    {'000': 0.5067, '110': 0.4933}
Transpiled probabilities:  {'000': 0.5009, '110': 0.4991}

4.2 Comparing Before and After ​

After transpilation, always compare the original and transpiled circuits on a simulator. The measurement probabilities should match within statistical noise. The workflow shown in section 4.1 demonstrates this pattern: run both circuits on CPUQVM and compare get_prob_dict() output. Each bitstring's probability should agree within a tolerance of approximately 0.05 for 10,000 shots.

4.3 Transpiling for Real Hardware ​

The transpiler handles circuit routing when the hardware topology constrains qubit connectivity. This example uses a 4-qubit linear chain where a cyclic CNOT pattern forces the transpiler to insert SWAP gates:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology

# 4-qubit linear chain: 0-1-2-3
chip_edges = generate_topology(4, "linear")

# Build a circuit with a cycle that forces routing
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.CNOT(1, 2)
prog << core.CNOT(2, 3)
prog << core.CNOT(3, 0)
prog << core.measure([0, 1, 2, 3], [0, 1, 2, 3])

print("=== Original Circuit ===")
print(f"Depth: {prog.depth(only_q2=True)}")
print(f"Gates: {prog.count_ops()}")
print(f"2Q Gates: {prog.count_ops(only_q2=True)}")

# Transpile with basic optimization
transpiler = Transpiler()
transpiled = transpiler.transpile(prog, chip_edges, {}, 1)

print("\n=== Transpiled Circuit (level 1) ===")
print(f"Depth: {transpiled.depth(only_q2=True)}")
print(f"Gates: {transpiled.count_ops()}")
print(f"2Q Gates: {transpiled.count_ops(only_q2=True)}")

# Verify on simulator
machine = core.CPUQVM()
machine.run(prog, 10000)
orig_probs = machine.result().get_prob_dict()

machine.run(transpiled, 10000)
tran_probs = machine.result().get_prob_dict()

print(f"\n=== Verification ===")
print(f"Original probabilities:    {orig_probs}")
print(f"Transpiled probabilities:  {tran_probs}")

Output:

=== Original Circuit ===
Depth: 4
Gates: {'CNOT': 4, 'H': 1}
2Q Gates: {'CNOT': 4}

=== Transpiled Circuit (level 1) ===
Depth: 5
Gates: {'SWAP': 2, 'CNOT': 4, 'U4': 1}
2Q Gates: {'SWAP': 2, 'CNOT': 4}

=== Verification ===
Original probabilities:    {'0000': 0.4961, '1110': 0.5039}
Transpiled probabilities:  {'0000': 0.4932, '1110': 0.5068}

The key advantage of transpiling locally is fine-grained control over the optimization level, initial mapping, and basic gate set. When targeting real quantum hardware, you would authenticate via the cloud service to obtain the chip topology and gate set, then apply the same transpilation workflow.


4.4 Fidelity-Aware Layout and Instruction Export ​

Starting in pyqpanda3 0.4.0, ChipBackend can carry calibration maps for single-qubit, readout, and two-qubit fidelity. You can either let the backend overload compute the layout internally with layout_method="fidelity", or call fidelity_layout() yourself to inspect the chosen mapping first.

python
from pyqpanda3.core import QProg, H, CZ, measure
from pyqpanda3.transpilation import ChipBackend, Transpiler, fidelity_layout

prog = QProg()
prog << H(0) << CZ(0, 1) << measure(0, 0) << measure(1, 1)

backend = ChipBackend()
backend.chip_topology_edges = [[0, 1], [1, 2]]
backend.basic_gates = ["RPhi", "CZ"]
backend.single_gate_fidelity = {0: 0.99, 1: 0.99, 2: 0.99}
backend.readout_fidelity = {0: 0.90, 1: 0.90, 2: 0.99}
backend.double_gate_fidelity = {(0, 1): 0.90, (1, 2): 0.99}

mapping = fidelity_layout(
    prog,
    backend.chip_topology_edges,
    backend.single_gate_fidelity,
    backend.readout_fidelity,
    backend.double_gate_fidelity,
)

transpiled = Transpiler().transpile(prog, backend, {}, 1, "fidelity")

print(mapping)
print(len(transpiled.operations()) > 0)

For instruction export, make sure the program is in the gate family expected by virtual_z(). A practical pattern is to decompose to ["X1", "RZ", "CZ"], convert to virtual-Z form, set backend timing, and then export scheduled JSON:

python
import json

from pyqpanda3.core import QProg, H, CZ, measure
from pyqpanda3.transpilation import ChipBackend, decompose, virtual_z

prog = QProg()
prog << H(0) << CZ(0, 1) << measure(0, 0) << measure(1, 1)

backend = ChipBackend()
backend.chip_topology_edges = [[0, 1], [1, 2]]
backend.basic_gates = ["RPhi", "CZ"]
backend.single_gate_timing = 40
backend.double_gate_timing = 80

instruction_ready = virtual_z(decompose(prog, ["X1", "RZ", "CZ"]))
instruction_json = instruction_ready.to_instruction(
    backend,
    offset=1,
    is_scheduling=True,
    compensate_pulse=False,
)
instructions = json.loads(instruction_json)

print(type(instruction_json).__name__, len(instructions))
print(instructions[0])

Output:

text
str 3
{'RPhi': [1, 270.0, 90.0, 0]}

If you need a longer cloud-backed workflow, see the dedicated Fidelity Layout with QCloud tutorial.

4.5 Advanced options: LightSABRE routing and transpilation metrics ​

When the default transpilation is not enough — limited connectivity, deep circuits, or you want to explicitly control the layout / routing strategy and recover metrics — use the 0.4.1 strongly-typed entry point TranspilationOptions, paired with TranspilationResult, which returns the transpiled program, compile time, and metrics in one call.

python
from pyqpanda3.core import QProg, H, CZ, measure
from pyqpanda3.transpilation import (
    ChipBackend,
    LayoutMethod,
    RoutingMethod,
    Transpiler,
    TranspilationOptions,
)

prog = QProg()
prog << H(0) << CZ(0, 1) << measure(0, 0) << measure(1, 1)

backend = ChipBackend()
backend.chip_topology_edges = [[0, 1], [1, 2], [1, 3]]
backend.basic_gates = ["RPhi", "CZ"]
backend.single_gate_fidelity = {0: 0.99, 1: 0.99, 2: 0.99, 3: 0.99}
backend.readout_fidelity = {0: 0.95, 1: 0.95, 2: 0.95, 3: 0.95}
backend.double_gate_fidelity = {(0, 1): 0.97, (1, 2): 0.96, (1, 3): 0.96}

options = TranspilationOptions()
options.optimization_level = 2
options.layout_method = LayoutMethod.FIDELITY
options.routing_method = RoutingMethod.LIGHT_SABRE
options.approximation_degree = 0.99
options.execution.routing_trials = 8
options.execution.seed = 42

result = Transpiler().transpile(prog, backend, options)

print(type(result).__name__)
print(result.compile_time_ns > 0)
print(sorted(result.metrics.keys()))
print(result.metrics["two_qubit_gate_count"])

Output (metric values depend on backend calibration):

TranspilationResult
True
['depth', 'fidelity_exact', 'fidelity_fallback', 'fidelity_unknown', 'log_expected_fidelity', 'two_qubit_gate_count']
1

Notes:

  • RoutingMethod.LIGHT_SABRE (also accepted as the string "light_sabre") enables the layered-lookahead router, which expands the interaction graph layer by layer and usually reduces two-qubit gate insertion and swap depth — useful for backends with limited connectivity or deeper circuits.
  • approximation_degree controls the fidelity-driven two-qubit gate approximation strength: 1.0 means exact, None uses the target two-qubit gate error rate, and a value in [0, 1] scales it.
  • result.metrics has the same shape as the dict returned by the standalone function transpilation_metrics(prog, backend), with two_qubit_gate_count, depth, log_expected_fidelity, and more. See Transpilation Options & Metrics.
  • 0.5.0: the layout and routing stack behind these options was rebuilt around a shared LightSABRE engine — bidirectional SABRE layout search with a balanced layout-routing objective, VF2 completion for embeddable subgraphs, ring-neighborhood layout candidates, and a work-budget circuit breaker for pathological instances. The Python API is unchanged, but compile times dropped substantially and output quality improved measurably (on the 1,044-case benchpress suite against Qiskit 2.5.2, pyqpanda3 0.5.0 is faster on 1,040 cases with a 0.233x geometric-mean time ratio while keeping the two-qubit gate count at 0.957x). Fixing execution.seed makes repeated transpilations of the same program produce identical output, and execution.max_threads / execution.compile_time_budget_ms bound the thread pool and wall-clock budget. See the benchmark page for the full analysis.

5. Practical Examples ​

5.1 Transpiling a GHZ Circuit ​

The Greenberger-Horne-Zeilinger (GHZ) state is a multi-qubit maximally entangled state. Preparing a GHZ state on real hardware requires the transpiler to route CNOT gates through the chip topology due to limited physical qubit connectivity.

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology

# Build a 4-qubit GHZ state circuit
# Logical circuit: H(0) -> CNOT(0,1) -> CNOT(0,2) -> CNOT(0,3)
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.CNOT(0, 2)
prog << core.CNOT(0, 3)
prog << core.measure([0, 1, 2, 3], [0, 1, 2, 3])

print("=== GHZ State Circuit ===")
print(f"Logical depth: {prog.depth(only_q2=True)}")
print(f"Logical gates: {prog.count_ops()}")

# Transpile for a linear chain topology
# Qubit 0 must interact with qubits 1, 2, 3
# In a linear chain: 0-1-2-3
# CNOT(0,2) and CNOT(0,3) require SWAP routing
chip_edges = generate_topology(4, "linear")
transpiler = Transpiler()
transpiled = transpiler.transpile(prog, chip_edges, {}, 2)

print("\n=== Transpiled GHZ Circuit ===")
print(f"Physical depth: {transpiled.depth(only_q2=True)}")
print(f"Physical gates: {transpiled.count_ops()}")
print(f"Physical 2Q gates: {transpiled.count_ops(only_q2=True)}")

# Verify
machine = core.CPUQVM()
machine.run(prog, 10000)
orig = machine.result().get_prob_dict()
machine.run(transpiled, 10000)
trans = machine.result().get_prob_dict()

print(f"\nOriginal GHZ:   {orig}")
print(f"Transpiled GHZ: {trans}")

Output:

=== GHZ State Circuit ===
Logical depth: 3
Logical gates: {'CNOT': 3, 'H': 1}

=== Transpiled GHZ Circuit ===
Physical depth: 4
Physical gates: {'SWAP': 1, 'CNOT': 3, 'H': 1}
Physical 2Q gates: {'SWAP': 1, 'CNOT': 3}

Original GHZ:   {'0000': 0.4952, '1111': 0.5048}
Transpiled GHZ: {'0000': 0.5073, '1111': 0.4927}

On a linear chain, the transpiler must route the CNOT(0,3) interaction through qubits 1 and 2. This introduces additional SWAP gates, increasing the circuit depth. On a fully connected topology, no routing is needed:

python
# No routing needed with all-to-all connectivity
full_topo = generate_topology(4, "all-to-all")
transpiled_full = transpiler.transpile(prog, full_topo, {}, 2)
print(f"\nAll-to-all depth: {transpiled_full.depth(only_q2=True)}")

Output:

All-to-all depth: 3

5.2 Transpiling a Random Circuit ​

Random circuits are useful for benchmarking transpilation performance. This example generates a random 3-qubit circuit and transpiles it on a 100-qubit square topology:

python
import time
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, generate_topology

qubits = list(range(3))
circuit = core.random_qcircuit(qubits, 20, ["RX", "RY", "RZ", "H", "CNOT", "SWAP"])

prog = core.QProg()
prog.append(circuit)
for q in qubits:
    prog.append(core.measure(q, q))

topo = generate_topology(100, "square")
transpiler = Transpiler()

start = time.time()
transpiled = transpiler.transpile(prog, topo, {}, 2)
elapsed = (time.time() - start) * 1000
print(f"Transpilation time: {elapsed:.2f} ms")
print(f"Before depth: {prog.depth(only_q2=True)}, After depth: {transpiled.depth(only_q2=True)}")

Output:

Transpilation time: 7.62 ms
Before depth: 17, After depth: 10

5.3 Decomposing a Unitary Matrix ​

Decompose a unitary matrix into a circuit and then verify it the resulting circuit matches the original matrix:

python
import numpy as np
from pyqpanda3.transpilation import decompose

# Custom U3 matrix
theta, phi, lam = np.pi / 5, np.pi / 7, np.pi / 11
u3_mat = np.array([
    [np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)],
    [np.exp(1j * phi) * np.sin(theta / 2),
     np.exp(1j * (phi + lam)) * np.cos(theta / 2)]
], dtype=complex)

circuit = decompose(u3_mat, [0])
print("Correct:", np.allclose(u3_mat, circuit.matrix()))

Output:

Correct: True

5.4 Custom Topology and Gate Set ​

The following example shows how to combine a custom T-shaped topology with a specific gate set:

python
from pyqpanda3 import core
from pyqpanda3.transpilation import Transpiler, decompose

# T-shaped topology: q0--q2--q3, q2--q1, q2--q4
t_topo = [[0, 2], [1, 2], [2, 3], [2, 4]]

prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)  # requires routing: 0->2->1
prog << core.CNOT(3, 4)  # requires routing: 3->2->4
prog << core.measure([0, 1, 2, 3, 4], [0, 1, 2, 3, 4])

transpiler = Transpiler()
transpiled = transpiler.transpile(prog, t_topo, {}, 2, basic_gates=["H", "RZ", "X1", "CNOT"])

print(f"Original: {prog.count_ops()} gates")
print(f"Transpiled: {transpiled.count_ops()} gates, {transpiled.count_ops(only_q2=True)} 2Q")

# Compare with decomposition only (no routing)
decomposed_only = decompose(prog, ["H", "RZ", "X1", "CNOT"])
print(f"Decomposed only: {decomposed_only.count_ops()} gates")

Output:

Original: {'CNOT': 2, 'H': 1} gates
Transpiled: {'RZ': 2, 'X1': 1, 'CNOT': 5} gates, {'CNOT': 5} 2Q
Decomposed only: {'CNOT': 2, 'H': 1} gates

6. Performance Considerations ​

Transpilation time depends on circuit size, topology sparsity, and optimization level. For circuits up to 100 qubits with depth 1000, transpilation typically completes in under a second.

Routing through sparse topologies increases circuit depth. If two qubits have a shortest-path distance of d in the topology graph (i.e., it takes d edges to traverse from one qubit to the other, where adjacent qubits have a distance of 1), a CNOT between them requires 2(d-1) SWAP gates to be inserted, and each SWAP consumes 3 CNOT gates, so the two-qubit gate count increases by 6(d-1).

DistanceSWAPs added2Q gate increase
1 (adjacent)00
2212
3424
d2(d-1)6(d-1)

Choosing a topology: Use "all-to-all" for decomposition testing, "linear" for worst-case routing, "square" for realistic chip simulation, use backend.chip_info() for real hardware.

Choosing an optimization level: Use 0 for debugging, 1 for quick testing, and 2 for production. For very large circuits (>50 qubits), level 1 may be faster than level 2.


7. API Quick Reference ​

generate_topology ​

SignatureReturnsDescription
generate_topology(num_qubits, topology_type)list[list[int]]Generate a chip topology edge list

Topology types: "linear", "square", "all-to-all"

Transpiler ​

MethodReturnsDescription
Transpiler()TranspilerCreate a new transpiler instance
.transpile(prog, edges, init_map, opt_level)QProgTranspile with edge list topology
.transpile(prog, backend, init_map, opt_level)QProgTranspile with backend object

decompose (3 overloads) ​

SignatureReturnsDescription
decompose(prog, basic_gates)QProgDecompose gates in a QProg
decompose(circuit, basic_gates)QCircuitDecompose gates in a QCircuit
decompose(matrix, qubits)QCircuitSynthesize circuit from unitary matrix

Summary ​

In this tutorial you learned:

  1. Why transpilation matters -- Real quantum hardware has a limited native gate set and restricted qubit connectivity. Transpilation bridges the gap between abstract circuits and hardware constraints.

  2. Chip topology -- generate_topology(n, type) creates synthetic topologies for testing. Three types are available: "linear" (chain), "square" (grid), and "all-to-all".

  3. The Transpiler class -- Transpiler().transpile() performs the full transpilation pipeline: gate decomposition, qubit routing via the SABRE algorithm, and circuit optimization. You can pass either an edge list or a cloud backend object.

  4. Optimization levels -- Level 0 does no optimization, level 1 applies basic gate cancellation, and level 2 applies aggressive optimization rules. Use level 2 for hardware execution.

  5. Gate decomposition -- The decompose function rewrites gates in terms of a specified basis set. Three overloads handle QProg, QCircuit, and raw unitary matrices.

  6. Verification -- Always compare the original and transpiled circuits on a simulator to confirm correctness before submitting to real hardware.

The transpilation module is the bridge between your quantum algorithms and real quantum processors. Understanding its capabilities helps you write circuits that are both correct and efficient on hardware.

Next steps:

Released under the MIT License.