Skip to content

Quantum Circuit Construction ​

Build quantum programs and reusable circuits using QProg, QCircuit, and the << operator in pyqpanda3.


Overview ​

pyqpanda3 provides two primary containers for building quantum circuits:

  • QProg -- the top-level quantum program that holds gates, circuits, measurements, and control flow. It is the unit you pass to a simulator or cloud backend for execution.
  • QCircuit -- a reusable, composable circuit module that encapsulates a sequence of quantum gates. Think of it as a named sub-routine you can embed inside any number of programs.

Both containers share the same append mechanism: the << (left-shift) operator. This operator chains gates, circuits, and even other programs into a single linear sequence of operations.

mermaid
graph TD
    subgraph "Container Hierarchy"
        QProg["QProg<br/><i>executable program</i>"]
        QCircuit["QCircuit<br/><i>reusable module</i>"]
        QGate["QGate<br/><i>single gate</i>"]
        Measure["measure()<br/><i>measurement</i>"]
    end

    QProg -->|"<< gate"| QGate
    QProg -->|"<< circuit"| QCircuit
    QProg -->|"<< prog"| QProg
    QProg -->|"<< measure"| Measure
    QCircuit -->|"<< gate"| QGate
    QCircuit -->|"<< circuit"| QCircuit
    QCircuit -->|"<< prog"| QProg

    style QProg fill:#4a90d9,color:#fff
    style QCircuit fill:#50b86c,color:#fff
    style QGate fill:#e8913a,color:#fff
    style Measure fill:#c0392b,color:#fff

1. QProg -- The Quantum Program ​

QProg is the main container for an executable quantum program. You create gates, append them to a program, and then execute the program on a simulator or cloud backend.

1.1 Creating a QProg ​

python
from pyqpanda3 import core

# Empty program -- qubit count is inferred from the gates you add
prog = core.QProg()

# Pre-allocated program with 3 qubits (and 0 classical bits)
prog = core.QProg(3)

When you create an empty QProg(), the system automatically tracks which qubits you use based on the gates and measurements you append. Pre-allocating with QProg(n) reserves n qubits upfront.

1.2 Appending Operations with << ​

The << operator is the primary way to build programs. It appends gates, circuits, measurements, and even other programs, and always returns the program itself to enable fluent chaining:

python
from pyqpanda3 import core

# Create a program and build a Bell state
prog = core.QProg()
prog << core.H(0)           # Hadamard on qubit 0
prog << core.CNOT(0, 1)     # CNOT: control=0, target=1
prog << core.measure([0, 1], [0, 1])  # Measure qubits [0,1] into cbits [0,1]

# Equivalent: chain everything in one expression
prog2 = core.QProg()
prog2 << core.H(0) << core.CNOT(0, 1) << core.measure([0, 1], [0, 1])

The flow of data through the operator chain is illustrated below:

mermaid
graph LR
    A["prog"] -->|"<<"| B["core.H(0)"]
    B -->|"returns prog"| C["prog"]
    C -->|"<<"| D["core.CNOT(0, 1)"]
    D -->|"returns prog"| E["prog"]
    E -->|"<<"| F["core.measure(...)"]
    F -->|"returns prog"| G["prog (complete)"]

    style A fill:#4a90d9,color:#fff
    style G fill:#4a90d9,color:#fff

You can also embed reusable circuits or absorb other programs:

python
# Embed a QCircuit
bell = core.QCircuit()
bell << core.H(0) << core.CNOT(0, 1)

prog = core.QProg()
prog << bell                    # append a circuit
prog << core.measure([0, 1], [0, 1])

# Absorb another QProg
prep = core.QProg()
prep << core.H(0)

main_prog = core.QProg()
main_prog << prep               # absorb a sub-program
main_prog << core.CNOT(0, 1)

1.3 QProg Methods ​

Once you have built a program, several methods let you inspect and transform it.

qubits_num() and cbits_num() ​

Return the number of qubits and classical bits used by the program:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

print(f"Qubits: {prog.qubits_num()}")  # Qubits: 2
print(f"Cbits:   {prog.cbits_num()}")   # Cbits:   2

运行结果:

Qubits: 2
Cbits:   2

depth() ​

Return the circuit depth -- the number of sequential gate layers. Gates that act on disjoint qubits can execute in parallel and count as the same layer. Use only_q2=True to count only two-qubit gates for depth:

python
prog = core.QProg()
prog << core.H(0) << core.H(1)  # depth 1: both H act on different qubits
prog << core.CNOT(0, 1)         # depth 2: CNOT depends on both qubits

print(f"Depth: {prog.depth()}")                    # Depth: 2
print(f"Depth (2Q only): {prog.depth(only_q2=True)}")  # Depth: 1

运行结果:

Depth: 2
Depth (2Q only): 1

count_ops() ​

Count gate operations by type. Returns a dictionary mapping gate names to their counts. Use only_q2=True to count only two-qubit gates:

python
prog = core.QProg()
prog << core.H(0) << core.H(1) << core.CNOT(0, 1) << core.RX(0, 1.57)

print(f"All gates: {prog.count_ops()}")                # All gates: {'H': 2, 'CNOT': 1, 'RX': 1}
print(f"2-qubit only: {prog.count_ops(only_q2=True)}")  # 2-qubit only: {'CNOT': 1}

运行结果:

All gates: {'RX': 1, 'CNOT': 1, 'H': 2}
2-qubit only: {'CNOT': 1}

flatten() ​

Recursively expand all nested circuits and sub-programs into a single flat sequence of operations:

python
inner = core.QCircuit()
inner << core.H(0) << core.CNOT(0, 1)

prog = core.QProg()
prog << inner << core.H(2)

flat = prog.flatten()  # new QProg with all nesting removed

to_circuit() ​

Convert the program to a QCircuit object for use with analysis tools that expect a circuit:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

circuit = prog.to_circuit()
print(f"Circuit qubits: {list(circuit.qubits())}")

运行结果:

Circuit qubits: [0, 1]

gate_operations() and operations() ​

Retrieve detailed information about the gates and operations in the program:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.measure([0, 1], [0, 1])

# Gate-level details
for g in prog.gate_operations():
    print(f"Gate: {g.name()}, qubits: {g.qubits()}")

# All operations including measurements
for op in prog.operations():
    print(f"Op type: {type(op).__name__}")

运行结果:

Gate: H, qubits: [0]
Gate: CNOT, qubits: [0, 1]
Op type: QGate
Op type: QGate
Op type: MeasureNode
Op type: MeasureNode

get_measure_nodes() ​

Extract measurement nodes from the program to inspect which qubits are measured and into which classical bits:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1) << core.measure([0, 1], [0, 1])

for node in prog.get_measure_nodes():
    print(f"Measure node: {node}")

运行结果:

Measure node: <pyqpanda3.core.core.MeasureNode object at 0x7be5421cedf0>
Measure node: <pyqpanda3.core.core.MeasureNode object at 0x7be5421cf3b0>

remap() ​

Remap qubit indices in the program. This is useful when running the same logical circuit on different physical qubits:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

remapped = prog.remap({0: 3, 1: 5})  # qubit 0 -> 3, qubit 1 -> 5
print(f"Remapped qubits: {remapped.qubits_num()}")

运行结果:

Remapped qubits: 6

originir() ​

Serialize the program to OriginIR string format -- the native intermediate representation used by pyqpanda3:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

print(prog.originir())
# QINIT 2
# CREG 2
# H q[0]
# CNOT q[0],q[1]
# MEASURE q[0],c[0]
# MEASURE q[1],c[1]

运行结果:

QINIT 2
CREG 2
H q[0]
CNOT q[0],q[1]
MEASURE q[0],c[0]
MEASURE q[1],c[1]

2. QCircuit -- Reusable Circuit Modules ​

QCircuit is a composable container for quantum gate sequences. Unlike QProg, it does not hold measurements or classical control flow. Its primary purpose is to let you define reusable building blocks that you can embed in multiple programs.

2.1 Creating and Building a QCircuit ​

python
from pyqpanda3 import core

# Empty circuit
circ = core.QCircuit()

# Pre-allocated circuit with 3 qubits
circ = core.QCircuit(3)

# Build using << or append()
bell = core.QCircuit()
bell << core.H(0) << core.CNOT(0, 1)

# append() is an alternative for programmatic construction
circ2 = core.QCircuit()
circ2.append(core.H(0))
circ2.append(core.CNOT(0, 1))

2.2 QCircuit Methods ​

size() and depth() ​

python
circ = core.QCircuit()
circ << core.H(0) << core.H(1) << core.CNOT(0, 1)

print(f"Gate count: {circ.size()}")  # Gate count: 3
print(f"Depth:      {circ.depth()}")  # Depth: 2

运行结果:

Gate count: 3
Depth:      2

qubits() ​

Return the set of qubit indices used by the circuit:

python
circ = core.QCircuit()
circ << core.H(0) << core.CNOT(0, 2)
print(f"Qubits used: {list(circ.qubits())}")  # Qubits used: [0, 2]

运行结果:

Qubits used: [0, 2]

count_ops() and num_2q_gate() ​

python
circ = core.QCircuit()
circ << core.H(0) << core.H(1) << core.CNOT(0, 1) << core.SWAP(1, 2)

print(f"All gates: {circ.count_ops()}")               # All gates: {'H': 2, 'CNOT': 1, 'SWAP': 1}
print(f"2-qubit only: {circ.count_ops(only_q2=True)}")  # 2-qubit only: {'CNOT': 1, 'SWAP': 1}
print(f"num_2q_gate: {circ.num_2q_gate()}")           # num_2q_gate: 2

运行结果:

All gates: {'SWAP': 1, 'CNOT': 1, 'H': 2}
2-qubit only: {'SWAP': 1, 'CNOT': 1}
num_2q_gate: 2

dagger() ​

Return the adjoint (conjugate transpose) of the circuit. This reverses the gate order and replaces each gate with its dagger -- useful for uncomputation:

python
circ = core.QCircuit()
circ << core.H(0) << core.S(1) << core.CNOT(0, 1)

inv_circ = circ.dagger()
# inv_circ = CNOT(0,1) -> Sdg(1) -> H(0)

control() ​

Return a controlled version of the circuit. Every gate gains an additional control qubit:

python
circ = core.QCircuit()
circ << core.H(0) << core.CNOT(0, 1)

controlled = circ.control([2])
# The entire circuit executes only when qubit 2 is |1>

expand() ​

Recursively expand nested circuits into a flat gate sequence:

python
inner = core.QCircuit()
inner << core.H(0) << core.CNOT(0, 1)

outer = core.QCircuit()
outer << inner << core.H(2)

expanded = outer.expand()

matrix() ​

Compute the unitary matrix representation of the circuit (feasible only for small circuits, as the dimension grows as 2n):

python
circ = core.QCircuit()
circ << core.H(0) << core.CNOT(0, 1)

U = circ.matrix()
print(f"Matrix shape: {U.shape}")  # Matrix shape: (4, 4)

运行结果:

Matrix shape: (4, 4)

For the Bell state circuit, the unitary is:

UBell=CNOT⋅(H⊗I)=12(10100101010−110−10)

set_name(), draw(), and originir() ​

python
circ = core.QCircuit(3)
circ << core.H(0) << core.CNOT(0, 1) << core.CNOT(1, 2)
circ.set_name("GHZ-3")

print(circ)               # text visualization
print(circ.originir())    # H q[0]\nCNOT q[0],q[1]\nCNOT q[1],q[2]

运行结果:

          ┌─┐               
q_0:  |0>─┤H├ ───*── ────── 
          └─┘ ┌──┴─┐        
q_1:  |0>──── ┤CNOT├ ───*── 
              └────┘ ┌──┴─┐ 
q_2:  |0>──── ────── ┤CNOT├ 
                     └────┘ 
 c :   / ══════════════════
                           


QINIT 3
CREG 1
H q[0]
CNOT q[0],q[1]
CNOT q[1],q[2]

2.3 Reusing Circuits ​

Define a building block once and use it everywhere:

python
from pyqpanda3 import core

def rotation_block(qc: int, qt: int, angle: float) -> core.QCircuit:
    block = core.QCircuit()
    block << core.H(qt)
    block << core.CU(qc, qt, 0, 0, angle, 0)
    return block

# Build a 3-qubit QFT using the reusable block
qft = core.QCircuit(3)
qft << core.H(0)
qft << rotation_block(0, 1, 1.5708)   # pi/2
qft << rotation_block(0, 2, 0.7854)   # pi/4
qft << core.H(1)
qft << rotation_block(1, 2, 1.5708)   # pi/2
qft << core.H(2)
qft << core.SWAP(0, 2)

# Use the same QFT in different programs
prog1 = core.QProg()
prog1 << qft << core.measure([0, 1, 2], [0, 1, 2])

prog2 = core.QProg()
prog2 << core.X(0) << qft << core.measure([0, 1, 2], [0, 1, 2])

3. The << Operator -- Appending Operations ​

The << operator is the central mechanism for building quantum programs and circuits. It is defined on both QProg and QCircuit and accepts several types of right-hand operands.

3.1 What You Can Append ​

Right-hand operandExampleDescription
QGateprog << core.H(0)Append a single quantum gate
QCircuitprog << circuitAppend all gates from a circuit
QProgprog << other_progAbsorb another program's operations
Measure resultprog << core.measure([0], [0])Append measurement operations

3.2 Appending Gates ​

python
from pyqpanda3 import core

prog = core.QProg()
prog << core.H(0)
prog << core.X(1) << core.Y(2) << core.Z(3)
prog << core.RX(0, 3.14159)
prog << core.U3(1, 0.5, 0.3, 0.1)

3.3 Appending Circuits and Programs ​

python
# Append a circuit
swap_circ = core.QCircuit()
swap_circ << core.CNOT(0, 1) << core.CNOT(1, 0) << core.CNOT(0, 1)

prog = core.QProg()
prog << core.H(0) << swap_circ

# Append a sub-program
prep = core.QProg()
prep << core.H(0) << core.H(1) << core.H(2)

main = core.QProg()
main << prep << core.CNOT(0, 1)

3.4 Appending Measurements ​

The core.measure() function creates measurement nodes that map quantum bits to classical bits:

python
prog = core.QProg()
prog << core.H(0) << core.CNOT(0, 1)

# Measure qubits [0, 1] into classical bits [0, 1]
prog << core.measure([0, 1], [0, 1])

# Measure a single qubit
prog << core.measure(0, 0)

3.5 Operator Chain Flow ​

Internally, each << call works like this:

mermaid
sequenceDiagram
    participant P as QProg
    participant G1 as H(0)
    participant G2 as CNOT(0,1)
    participant M as measure(...)

    P->>G1: prog << core.H(0)
    G1-->>P: returns prog (now contains H(0))
    P->>G2: prog << core.CNOT(0, 1)
    G2-->>P: returns prog (now contains H(0), CNOT)
    P->>M: prog << core.measure(...)
    M-->>P: returns prog (complete program)

4. Gate Construction ​

pyqpanda3 provides 37+ quantum gates organized into single-qubit, two-qubit, and multi-qubit categories. All gates are constructed as factory functions in the core module.

4.1 Single-Qubit Gates ​

Non-parameterized ​

python
from pyqpanda3 import core

# Pauli gates
core.X(0)   # Pauli-X (bit flip)
core.Y(0)   # Pauli-Y
core.Z(0)   # Pauli-Z (phase flip)

# Clifford gates
core.H(0)   # Hadamard
core.S(0)   # S gate (sqrt of Z)
core.T(0)   # T gate (sqrt of S)

# Identity and variants
core.I(0)   # Identity
core.X1(0)  # X1 gate
core.Y1(0)  # Y1 gate
core.Z1(0)  # Z1 gate

# Special
core.ECHO(0)    # Echo gate (with optional parameter)
core.BARRIER([0]) # Barrier (prevents optimization across this point)

Parameterized ​

python
from pyqpanda3 import core
import math

# Rotation gates
core.RX(0, math.pi / 2)     # Rotation around X axis
core.RY(0, math.pi / 4)     # Rotation around Y axis
core.RZ(0, math.pi / 3)     # Rotation around Z axis

# Phase gate
core.P(0, math.pi / 6)      # Phase gate (diag(1, e^{i\theta}))

# General unitary gates
core.U1(0, math.pi / 4)             # 1-parameter unitary
core.U2(0, 0, math.pi / 2)          # 2-parameter unitary
core.U3(0, 0.1, 0.2, 0.3)           # 3-parameter unitary
core.U4(0, 0.1, 0.2, 0.3, 0.4)     # 4-parameter unitary (most general 1Q gate)

The rotation gates follow standard definitions:

RX(θ)=e−iθX/2=(cos⁡(θ/2)−isin⁡(θ/2)−isin⁡(θ/2)cos⁡(θ/2))RY(θ)=e−iθY/2=(cos⁡(θ/2)−sin⁡(θ/2)sin⁡(θ/2)cos⁡(θ/2))RZ(θ)=e−iθZ/2=(e−iθ/200eiθ/2)

4.2 Two-Qubit Gates ​

Non-parameterized ​

python
core.CNOT(0, 1)      # Controlled-X: control=0, target=1
core.CZ(0, 1)        # Controlled-Z
core.SWAP(0, 1)      # Swap qubits 0 and 1
core.ISWAP(0, 1)     # iSWAP gate
core.SQISWAP(0, 1)   # Square root of iSWAP
core.MS(0, 1)        # Molmer-Sorensen (ion trap)

Parameterized ​

python
from pyqpanda3 import core
import math

core.CRX(0, 1, math.pi / 2)   # Controlled-RX
core.CRY(0, 1, math.pi / 3)   # Controlled-RY
core.CRZ(0, 1, math.pi / 4)   # Controlled-RZ
core.CP(0, 1, math.pi / 6)    # Controlled phase
core.CU(0, 1, 0.1, 0.2, 0.3, 0.4)  # 4-parameter controlled unitary
core.RXX(0, 1, math.pi / 4)   # XX rotation
core.RYY(0, 1, math.pi / 4)   # YY rotation
core.RZZ(0, 1, math.pi / 4)   # ZZ rotation
core.RZX(0, 1, math.pi / 4)   # ZX rotation

The two-qubit rotation gates: RXX(θ)=e−iθ(X⊗X)/2, RYY(θ)=e−iθ(Y⊗Y)/2, RZZ(θ)=e−iθ(Z⊗Z)/2.

4.3 Multi-Qubit Gates ​

python
core.TOFFOLI(0, 1, 2)  # CCX: flips target=2 if controls 0,1 are both |1>

4.4 Special Gates ​

BARRIER prevents the compiler from merging or reordering gates across the barrier point:

python
prog = core.QProg()
prog << core.H(0) << core.H(1)
prog << core.BARRIER([0, 1])  # no optimization across this line
prog << core.CNOT(0, 1)

RESET (new in 0.5.0) is the native, non-unitary reset channel: it collapses the qubit to |0> with certainty and leaves the classical registers untouched — no more measure-plus-conditional-X emulation. It is supported on the CPU state-vector and density-matrix simulators and round-trips through OriginIR:

python
from pyqpanda3.core import CPUQVM, QProg, RESET, X, measure

prog = QProg() << X(0) << RESET(0) << measure(0, 0)
qvm = CPUQVM()
qvm.run(prog, 100)
print(qvm.result().get_counts())  # {'0': 100} -- the X was undone by RESET

ECHO is a specialized gate used in certain pulse-level calibrations. It accepts an optional parameter:

python
echo_gate = core.ECHO(0)
echo_gate.set_parameters([1.5])  # set custom parameter

Oracle constructs a quantum oracle from a unitary matrix:

python
import numpy as np
from pyqpanda3 import core

# Oracle from a unitary matrix (e.g., marking |11>)
oracle_matrix = np.eye(4, dtype=complex)
oracle_matrix[3, 3] = -1  # flip phase for |11>
prog = core.QProg()
prog << core.H(0) << core.H(1)
prog << core.Oracle([0, 1], oracle_matrix)

create_gate() constructs a gate from its type enum and parameters:

python
gate = core.create_gate("H", [0], [])           # H gate on qubit 0
gate = core.create_gate("RX", [0], [1.57])      # RX gate with angle

4.5 Gate Introspection and Transformations ​

Every gate object supports introspection:

python
gate = core.RX(0, 1.57)
print(gate.name())            # "RX"
print(gate.gate_type())       # GateType.RX
print(gate.target_qubits())   # [0]
print(gate.control_qubits())  # []
print(gate.parameters())      # [1.57]
print(gate.qubits())          # [0]

运行结果:

RX
GateType.RX
[0]
[]
[1.57]
[0]

Every gate also supports dagger(), control(), and matrix():

python
from pyqpanda3 import core
import numpy as np

# dagger() returns the adjoint (inverse)
gate = core.H(0)           # H is self-adjoint: H.dagger() = H
inv = gate.dagger()
m1, m2 = gate.matrix(), inv.matrix()
print(np.allclose(np.dot(m1, m2), np.eye(2)))  # True

# control() returns a controlled version
ch = core.H(0).control([2])
print(ch.qubits())  # [2, 0]

# matrix() computes the unitary
print(f"H matrix:\n{core.H(0).matrix()}")
print(f"CNOT matrix shape: {core.CNOT(0, 1).matrix().shape}")  # (4, 4)

运行结果:

True
[2, 0]
H matrix:
[[ 0.70710678+0.j  0.70710678+0.j]
 [ 0.70710678+0.j -0.70710678+0.j]]
CNOT matrix shape: (4, 4)

5. DAG-Based Circuit Analysis ​

pyqpanda3 provides DAGQCircuit for Directed Acyclic Graph-based analysis of quantum circuits. This enables layer decomposition, dependency analysis, and critical path computation.

5.1 What Is a DAG? ​

A DAG represents the dependency structure of gates in a circuit. Each gate is a node, and edges represent qubit dependencies: if gate B uses the output of gate A on the same qubit, there is an edge from A to B.

mermaid
graph TD
    subgraph "Circuit: H(0), H(1), CNOT(0,1), X(2)"
        H0["H(0)"] --> CNOT["CNOT(0,1)"]
        H1["H(1)"] --> CNOT
        X2["X(2)"]
    end

    subgraph "DAG Layers"
        L1["Layer 0<br/>H(0), H(1), X(2)"]
        L2["Layer 1<br/>CNOT(0,1)"]
        L1 --> L2
    end

    style H0 fill:#e8913a,color:#fff
    style H1 fill:#e8913a,color:#fff
    style X2 fill:#e8913a,color:#fff
    style CNOT fill:#4a90d9,color:#fff

5.2 Building a DAGQCircuit ​

python
from pyqpanda3 import core

circ = core.QCircuit()
circ << core.H(0) << core.H(1) << core.CNOT(0, 1) << core.X(2)

# From QCircuit directly
dag = core.DAGQCircuit()
dag.from_circuit(circ)
dag.build()

# From QProg via to_circuit()
prog = core.QProg()
prog << circ
dag2 = core.DAGQCircuit()
c = prog.to_circuit()
if c is not None:
    dag2.from_circuit(c)
    dag2.build()

5.3 Inspecting the DAG ​

python
# All gates
for gate in dag.gates():
    print(f"{gate.name()} on qubits {gate.qubits()}")

# Nodes and edges
print(f"Nodes: {list(dag.nodes())}")
print(f"Edges: {list(dag.edges())}")

# Gate at a specific node index
for idx in dag.nodes():
    gate = dag.get_gate(idx)
    print(f"Node {idx}: {gate.name()}{gate.qubits()}")

运行结果:

H on qubits [0]
H on qubits [1]
CNOT on qubits [0, 1]
X on qubits [2]
Nodes: [0, 1, 2, 3]
Edges: [(0, 2), (1, 2)]
Node 0: H[0]
Node 1: H[1]
Node 2: CNOT[0, 1]
Node 3: X[2]

5.4 Layer Analysis ​

python
dag = core.DAGQCircuit()
circ = core.QCircuit()
circ << core.H(0) << core.H(1) << core.H(2)
circ << core.CNOT(0, 1) << core.CNOT(1, 2)
dag.from_circuit(circ)
dag.build()

print(f"DAG depth: {dag.get_depth()}")  # 3

for i, layer in enumerate(dag.layers()):
    names = [dag.get_gate(idx).name() for idx in layer]
    print(f"Layer {i}: {names}")
# Layer 0: ['H', 'H', 'H']
# Layer 1: ['CNOT']
# Layer 2: ['CNOT']

运行结果:

DAG depth: 3
Layer 0: ['H', 'H', 'H']
Layer 1: ['CNOT']
Layer 2: ['CNOT']

5.5 Critical Path and Two-Qubit Gate Analysis ​

python
# Longest path = critical path
path = dag.longest_path()
for node in path:
    gate = dag.get_gate(node)
    print(f"  {gate.name()} on qubits {gate.qubits()}")

# Two-qubit gate analysis
two_q_gates = dag.two_qubit_gates()
for gate in two_q_gates:
    if gate.is_controlled():
        print(f"  Controlled: ctrl={gate.control_qubits()}, tgt={gate.target_qubits()}")
    else:
        print(f"  Non-controlled: qubits={gate.target_qubits()}")

two_q_nodes = list(dag.two_qubit_gate_nodes())

运行结果:

  H on qubits [0]
  CNOT on qubits [0, 1]
  CNOT on qubits [1, 2]
  Non-controlled: qubits=[0, 1]
  Non-controlled: qubits=[1, 2]

5.6 Practical Example: Comparing Circuits ​

python
from pyqpanda3 import core

# Naive SWAP using 3 CNOTs vs native SWAP gate
swap_naive = core.QCircuit()
swap_naive << core.CNOT(0, 1) << core.CNOT(1, 0) << core.CNOT(0, 1)

swap_native = core.QCircuit()
swap_native << core.SWAP(0, 1)

for name, circ in [("Naive SWAP", swap_naive), ("Native SWAP", swap_native)]:
    dag = core.DAGQCircuit()
    dag.from_circuit(circ)
    dag.build()
    print(f"--- {name} ---")
    print(f"  Gates: {len(dag.gates())}, Depth: {dag.get_depth()}, "
          f"2Q gates: {len(dag.two_qubit_gates())}")

运行结果:

--- Naive SWAP ---
  Gates: 3, Depth: 3, 2Q gates: 3
--- Native SWAP ---
  Gates: 1, Depth: 1, 2Q gates: 1

6. Complete Examples ​

6.1 Bell State with Full Analysis ​

python
from pyqpanda3 import core

bell = core.QCircuit()
bell << core.H(0) << core.CNOT(0, 1)

prog = core.QProg()
prog << bell << core.measure([0, 1], [0, 1])

# Statistics
print(f"Qubits: {prog.qubits_num()}, Depth: {prog.depth()}, "
      f"Gates: {prog.count_ops()}, 2Q: {prog.count_ops(only_q2=True)}")
# Gates: {'H': 1, 'CNOT': 1}, 2Q: {'CNOT': 1}

# DAG analysis
dag = core.DAGQCircuit()
dag.from_circuit(bell)
dag.build()
print(f"DAG depth: {dag.get_depth()}")
for i, layer in enumerate(dag.layers()):
    print(f"  Layer {i}: {[dag.get_gate(idx).name() for idx in layer]}")

# Run on simulator
machine = core.CPUQVM()
machine.run(prog, 1000)
print(f"Results: {machine.result().get_counts()}")
print(f"OriginIR:\n{prog.originir()}")

运行结果:

Qubits: 2, Depth: 2, Gates: {'CNOT': 1, 'H': 1}, 2Q: {'CNOT': 1}
DAG depth: 2
  Layer 0: ['H']
  Layer 1: ['CNOT']
Results: {'11': 513, '00': 487}
OriginIR:
QINIT 2
CREG 2
H q[0]
CNOT q[0],q[1]
MEASURE q[0],c[0]
MEASURE q[1],c[1]

6.2 Uncomputation Pattern ​

python
from pyqpanda3 import core

compute = core.QCircuit()
compute << core.CNOT(0, 2) << core.CNOT(1, 2)  # XOR onto qubit 2

use = core.QCircuit()
use << core.Z(2)

uncompute = compute.dagger()

prog = core.QProg()
prog << compute << use << uncompute
prog << core.measure([0, 1, 2], [0, 1, 2])

machine = core.CPUQVM()
machine.run(prog, 1000)
# Auxiliary qubit 2 should always return to |0>

6.3 Controlled Circuit ​

python
from pyqpanda3 import core

sub = core.QCircuit()
sub << core.H(0) << core.RX(1, 0.5) << core.CNOT(0, 1)

controlled_sub = sub.control([3])

prog = core.QProg()
prog << core.H(3) << controlled_sub
prog << core.measure([0, 1, 3], [0, 1, 3])

6.4 Qubit Remapping ​

python
from pyqpanda3 import core

logical = core.QProg()
logical << core.H(0) << core.CNOT(0, 1) << core.measure([0, 1], [0, 1])

physical = logical.remap({0: 5, 1: 7})  # map to physical qubits 5, 7
print(f"Physical OriginIR:\n{physical.originir()}")

运行结果:

Physical OriginIR:
QINIT 8
CREG 2
H q[5]
CNOT q[5],q[7]
MEASURE q[5],c[0]
MEASURE q[7],c[1]

7. API Quick Reference ​

QProg ​

MethodReturnsDescription
core.QProg() / core.QProg(n)QProgCreate empty or pre-allocated program
prog << xQProgAppend gate, circuit, program, or measurement
prog.qubits_num()intNumber of qubits used
prog.cbits_num()intNumber of classical bits used
prog.depth(only_q2)intCircuit depth (optionally count only 2-qubit gates)
prog.count_ops(only_q2)dict[str, int]Gate count by name (optionally only 2-qubit)
prog.flatten()QProgFlatten all nested sub-circuits
prog.to_circuit()QCircuitConvert to QCircuit
prog.gate_operations()listList of gate operations
prog.operations()listList of all operations
prog.get_measure_nodes()listExtract measurement nodes
prog.remap(mapping)QProgRemap qubit indices
prog.originir()strSerialize to OriginIR string

QCircuit ​

MethodReturnsDescription
core.QCircuit() / core.QCircuit(n)QCircuitCreate empty or pre-allocated circuit
circ << x / circ.append(gate)QCircuitAppend gate, circuit, or program
circ.size()intTotal gate count
circ.depth()intCircuit depth
circ.qubits()setSet of qubit indices used
circ.count_ops(only_q2)dict[str, int]Gate count by name (optionally only 2-qubit)
circ.num_2q_gate()intNumber of two-qubit gates
circ.dagger()QCircuitAdjoint (inverse) circuit
circ.control(qubits)QCircuitControlled version of circuit
circ.expand()QCircuitExpand nested sub-circuits
circ.matrix()ndarrayUnitary matrix representation
circ.set_name(name)NoneSet a human-readable name
circ.originir()strSerialize to OriginIR

DAGQCircuit ​

MethodReturnsDescription
dag.from_circuit(circ)NoneLoad gates from a QCircuit
dag.build()NoneConstruct the DAG edges
dag.gates()listAll gate objects
dag.nodes() / dag.edges()listNode indices / edge pairs
dag.get_gate(idx)QGateGate at node index
dag.layers()iteratorGate layers (parallel groups)
dag.get_depth()intDAG depth (number of layers)
dag.longest_path()listCritical path through DAG
dag.two_qubit_gates()listAll two-qubit gate objects
dag.two_qubit_gate_nodes()listNode indices of 2Q gates

Gate Factory Functions ​

CategoryGates
1Q non-parameterizedH, X, Y, Z, S, T, I, X1, Y1, Z1, ECHO
1Q parameterizedRX, RY, RZ, P, RPhi, U1, U2, U3, U4
2Q non-parameterizedCNOT, CZ, SWAP, ISWAP, SQISWAP, MS
2Q parameterizedCP, CR, CRX, CRY, CRZ, CU, RXX, RYY, RZZ, RZX
Multi-qubitTOFFOLI
SpecialBARRIER, ECHO, Oracle, create_gate()

Summary ​

In this tutorial you learned:

  1. QProg is the executable quantum program container. Use << to append gates, circuits, measurements, and other programs. Inspect it with depth(), count_ops(), qubits_num(), and more.

  2. QCircuit is a reusable circuit module. It supports the same << operator plus additional methods like dagger(), control(), matrix(), and num_2q_gate().

  3. The << operator is the universal append mechanism. It returns the container, enabling fluent chains like prog << core.H(0) << core.CNOT(0, 1).

  4. Gate construction covers 37+ gates from single-qubit (H, X, RX, U3) to two-qubit (CNOT, SWAP, CU) to multi-qubit (TOFFOLI), plus special gates like BARRIER and Oracle.

  5. DAGQCircuit enables advanced circuit analysis: layer decomposition, critical path finding, two-qubit gate extraction, and depth computation through a directed acyclic graph representation.

The next tutorial in the series covers Simulation, where you will learn how to execute the programs you built here on various simulator backends.


Knowledge Check ​

Test your understanding of circuit construction in pyqpanda3.

Q1: What is the difference between QProg and QCircuit? When would you use each?

A1: QProg is the top-level executable quantum program container -- it can hold gates, circuits, measurements, and control flow. QCircuit is a reusable circuit module that supports additional operations like dagger(), control(), and matrix(). Use QProg as the main program wrapper and QCircuit for building reusable sub-circuits that can be inverted, controlled, or composed.

Q2: What does the << operator return, and why is it useful?

A2: The << operator returns the container (QProg or QCircuit) itself, enabling method chaining. This allows fluent construction like prog << H(0) << CNOT(0, 1) << measure([0, 1], [0, 1]) without needing intermediate variables for every step.

Q3: How do you create the adjoint (inverse) of a circuit? Give an example with a practical use case.

A3: Call .dagger() on a QCircuit: inverse_cir = cir.dagger(). This is essential for uncomputation -- after using ancilla qubits in a computation, applying the inverse circuit restores them to |0⟩ so they can be reused. For example, in Grover's algorithm, the oracle and diffusion operator both use dagger().

Q4: Explain the relationship between BARRIER, ECHO, and IDLE gates. Why do they have no mathematical effect?

A4: All three are meta-operations with identity unitary matrices (I). BARRIER prevents gate fusion across it during optimization, ECHO represents a spin echo sequence for noise suppression, and IDLE represents a time delay. They affect compilation and scheduling but not the mathematical state evolution.

Q5: What is a DAGQCircuit and what advantages does it offer over a flat circuit representation?

A5: DAGQCircuit represents the circuit as a Directed Acyclic Graph, where gates are nodes and dependencies (shared qubits) are edges. This enables advanced analysis: finding the critical path (circuit depth), layer decomposition for parallel gate execution, counting two-qubit gates, and topology-aware routing during transpilation.

Q6: How would you construct a controlled-SWAP (Fredkin) gate using existing pyqpanda3 primitives?

A6: A Fredkin gate (controlled-SWAP) can be decomposed as: CNOT(b, c) << TOFFOLI(a, c, b) << CNOT(b, c) where a is the control and b, c are the SWAP targets. Alternatively, use SWAP(1, 2).control(0) to create a controlled version of the SWAP circuit.


Exercise 1: Reusable Circuit Module

Create a parameterized "quantum half-adder" circuit and demonstrate composing it into a program with dagger() for uncomputation.

Solution:

python
from pyqpanda3.core import CPUQVM, QProg, QCircuit, CNOT, TOFFOLI, X, measure

def quantum_half_adder(a, b, carry):
    """1-bit quantum half-adder (TOFFOLI first to preserve original values).
    Input:  a, b (1-qubit inputs), carry (initialized to |0>)
    Output: b = a XOR b (sum), carry = a AND b
    """
    cir = QCircuit()
    cir << TOFFOLI(a, b, carry)  # carry = a AND b (original values)
    cir << CNOT(a, b)            # b = a XOR b (sum)
    return cir

qvm = CPUQVM()

# Test all four input combinations
for a_val, b_val in [(0, 0), (0, 1), (1, 0), (1, 1)]:
    prog = QProg()
    if a_val:
        prog << X(0)  # Set a=1
    if b_val:
        prog << X(1)  # Set b=1

    adder = quantum_half_adder(0, 1, 2)
    prog << adder
    prog << measure([1, 2], [0, 1])

    qvm.run(prog, shots=100)
    counts = qvm.result().get_counts()
    # result bits: (sum=b, carry=c) — e.g. "10" = sum=1, carry=0
    print(f"{a_val}+{b_val}: {counts}")

# Demonstrate uncompute
prog2 = QProg()
prog2 << X(0) << X(1)          # a=1, b=1 → 1+1
adder = quantum_half_adder(0, 1, 2)
prog2 << adder
prog2 << adder.dagger()        # Uncompute
prog2 << measure([0, 1, 2], [0, 1, 2])
qvm.run(prog2, shots=100)
counts2 = qvm.result().get_counts()
print(f"After uncompute (1+1): {counts2}")

运行结果:

0+0: {'00': 100}
0+1: {'10': 100}
1+0: {'10': 100}
1+1: {'01': 100}
After uncompute (1+1): {'110': 100}

Note: Extending to a full 2-bit ripple-carry adder requires additional gates (MAJ/UMA blocks) to correctly propagate intermediate carries. The half-adder above demonstrates the key principle: compute the carry using TOFFOLI on the original input values BEFORE modifying them with CNOT.

Exercise 2: DAGQCircuit Analysis

Build a 4-qubit circuit, convert it to a DAGQCircuit, and analyze its properties: depth, two-qubit gate count, and layer structure.

Solution:

python
from pyqpanda3.core import QProg, QCircuit, DAGQCircuit
from pyqpanda3.core import H, X, CNOT, SWAP, CZ, TOFFOLI, measure

# Build a circuit with mixed gate types
cir = QCircuit()
cir << H(0) << H(1) << CNOT(0, 1) << CNOT(2, 3)
cir << SWAP(1, 2) << CZ(0, 3)
cir << TOFFOLI(0, 1, 2) << H(3)

dag = DAGQCircuit()
dag.from_circuit(cir)
dag.build()

# Analyze properties
print("DAG Circuit Analysis:")
print(f"  Depth: {dag.get_depth()}")
print(f"  Total gates: {dag.get_num_gates()}")
print(f"  Two-qubit gates: {len(dag.two_qubit_gates())}")

# Layer decomposition
layers = dag.layers()
for i, layer in enumerate(layers):
    gate_names = [dag.get_gate(idx).name() for idx in layer]
    print(f"  Layer {i}: {gate_names}")

运行结果:

DAG Circuit Analysis:
  Depth: 4
  Total gates: 8
  Two-qubit gates: 4
  Layer 0: ['H', 'H', 'CNOT']
  Layer 1: ['CNOT']
  Layer 2: ['SWAP', 'CZ']
  Layer 3: ['TOFFOLI', 'H']

Released under the MIT License.