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
- The Transpilation Pipeline
- 1. Chip Topology
- 2. The Transpiler Class
- 3. Gate Decomposition
- 4. Putting It All Together
- 5. Practical Examples
- 6. Performance Considerations
- 7. API Quick Reference
- Summary
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.
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:#2d8a4eThe Transpilation Pipeline
Transpilation in pyqpanda3 consists of several stages, each addressing one or more hardware constraints:
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:#fffGate 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:
| Function | Purpose |
|---|---|
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 --- q3A 4-qubit square (grid) topology adds diagonal connections:
q0 --- q1
| |
q2 --- q3The 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.
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:
generate_topology(num_qubits: int, topology_type: str) -> listParameters:
| Parameter | Type | Description |
|---|---|---|
num_qubits | int | Number of physical qubits in the topology |
topology_type | str | The 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:
| Type | Description | Best For |
|---|---|---|
"linear" | Chain: q0-q1-q2-...-q(n-1) | Simple test circuits |
"square" | 2D grid arrangement | Grid-based QPUs |
"all-to-all" | Every qubit connected to every other | Idealized testing |
Linear Topology
A linear chain connects each qubit to its immediate neighbors:
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]]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:#fffSquare (Grid) Topology
A square topology arranges qubits in a 2D grid. Each qubit is connected to its neighbors horizontally and vertically:
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:
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:
custom_topo = [[0, 1], [0, 2], [0, 3], [0, 4]]Or a ring (circular) topology:
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:
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:
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 3In 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:
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)
) -> QProgParameters:
| Parameter | Type | Description |
|---|---|---|
prog | QProg | The quantum program to transpile |
chip_topology_edges | list[list[int]] | Edge list of the chip topology |
init_mapping | dict | Initial logical-to-physical qubit mapping. Pass {} for automatic. |
optimization_level | int | Optimization level: 0 (none), 1 (basic), 2 (aggressive) |
basic_gates | list[str] or None | Target 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:
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 succeededWhen 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.
| Level | Behavior | Use Case |
|---|---|---|
0 | No optimization. Only routing and decomposition. | Debugging; seeing raw routing output |
1 | Basic optimization. Cancels adjacent inverse gates and merges rotations. | Fast transpilation with cleanup |
2 | Aggressive optimization. All rewriting rules including commutativity-based reordering. | Production circuits for hardware |
# 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:
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 mappingWhen 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:
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 setCommon gate sets:
| Gate Set | Gates | Hardware |
|---|---|---|
| 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:
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.
Circuit analysis: Decomposing into a standard gate set makes it easier to count resources (e.g., how many two-qubit gates are actually needed).
Gate count optimization: A high-level gate like
TOFFOLIrequires 6 CNOT gates when decomposed. Knowing this helps you estimate circuit cost.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:
SWAP(a, b)=CNOT(a,b)+CNOT(b,a)+CNOT(a,b)(3 CNOT gates)CZ(a, b)=H(b)+CNOT(a,b)+H(b)(1 CNOT + 2 H gates)TOFFOLI(a, b, c)= 6 CNOT + several single-qubit gates
3.2 decompose(prog, basic_gates)
Decompose all gates in a QProg into the specified basic gate set:
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:
decompose(prog: QProg, basic_gates: list) -> QProgParameters:
| Parameter | Type | Description |
|---|---|---|
prog | QProg | Input quantum program |
basic_gates | list[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:
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: 7Signature:
decompose(circuit: QCircuit, basic_gates: list) -> QCircuitParameters:
| Parameter | Type | Description |
|---|---|---|
circuit | QCircuit | Input quantum circuit |
basic_gates | list[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
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: 1Signature:
decompose(matrix: np.ndarray, qubits: list) -> QCircuitParameters:
| Parameter | Type | Description |
|---|---|---|
matrix | np.ndarray | Unitary matrix to decompose. Must be n qubits. |
qubits | list[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:
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:
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: TrueOutput:
Matrices match: True3.5 Decomposition Rules
The decomposition engine applies well-known quantum circuit identities. Here are the most important ones:
SWAP decomposition:
This requires 3 CNOT gates.
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:
TOFFOLI decomposition:
A Toffoli (CCX) gate decomposes into 6 CNOT gates and 7 single-qubit gates:
Single-qubit gate decomposition:
Any single-qubit unitary
Or into a U3 gate:
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:
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:#fffComplete example:
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:
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.
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:
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:
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.
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']
1RoutingMethod.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_degreecontrols the fidelity-driven two-qubit gate approximation strength:1.0means exact,Noneuses the target two-qubit gate error rate, and a value in[0, 1]scales it.result.metricshas the same shape as the dict returned by the standalone functiontranspilation_metrics(prog, backend), withtwo_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.seedmakes repeated transpilations of the same program produce identical output, andexecution.max_threads/execution.compile_time_budget_msbound 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.
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:
# 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: 35.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:
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: 105.3 Decomposing a Unitary Matrix
Decompose a unitary matrix into a circuit and then verify it the resulting circuit matches the original matrix:
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: True5.4 Custom Topology and Gate Set
The following example shows how to combine a custom T-shaped topology with a specific gate set:
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} gates6. 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).
| Distance | SWAPs added | 2Q gate increase |
|---|---|---|
| 1 (adjacent) | 0 | 0 |
| 2 | 2 | 12 |
| 3 | 4 | 24 |
d | 2(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
| Signature | Returns | Description |
|---|---|---|
generate_topology(num_qubits, topology_type) | list[list[int]] | Generate a chip topology edge list |
Topology types: "linear", "square", "all-to-all"
Transpiler
| Method | Returns | Description |
|---|---|---|
Transpiler() | Transpiler | Create a new transpiler instance |
.transpile(prog, edges, init_map, opt_level) | QProg | Transpile with edge list topology |
.transpile(prog, backend, init_map, opt_level) | QProg | Transpile with backend object |
decompose (3 overloads)
| Signature | Returns | Description |
|---|---|---|
decompose(prog, basic_gates) | QProg | Decompose gates in a QProg |
decompose(circuit, basic_gates) | QCircuit | Decompose gates in a QCircuit |
decompose(matrix, qubits) | QCircuit | Synthesize circuit from unitary matrix |
Summary
In this tutorial you learned:
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.
Chip topology --
generate_topology(n, type)creates synthetic topologies for testing. Three types are available:"linear"(chain),"square"(grid), and"all-to-all".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.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.
Gate decomposition -- The
decomposefunction rewrites gates in terms of a specified basis set. Three overloads handleQProg,QCircuit, and raw unitary matrices.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:
- Cloud Computing -- Submit transpiled circuits to real quantum processors
- Visualization -- Draw and compare original vs. transpiled circuits
- Noise Simulation -- Simulate noise on transpiled circuits
- API Reference: transpilation -- Complete API documentation