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.


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:

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

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

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}

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())}")

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__}")

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}")

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()}")

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])

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

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]

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

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)

For the Bell state circuit, the unitary is:

UBell=CNOT(HI)=12(1001011001101001)

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])

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:


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
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 (alternative: RPhi)

# 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(θ)=eiθX/2=(cos(θ/2)isin(θ/2)isin(θ/2)cos(θ/2))RY(θ)=eiθY/2=(cos(θ/2)sin(θ/2)sin(θ/2)cos(θ/2))RZ(θ)=eiθZ/2=(eiθ/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
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(θ)=eiθ(XX)/2, RYY(θ)=eiθ(YY)/2, RZZ(θ)=eiθ(ZZ)/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)

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 truth table or binary function:

python
oracle = core.Oracle(qubits, truth_table)

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]

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

python
import numpy as np

# dagger() returns the adjoint (inverse)
gate = core.RX(0, 1.57)
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)

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.

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()}")

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']

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())

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())}")

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()}")

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()}")

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 adder" circuit that adds two 2-bit numbers encoded in qubits. Demonstrate using it in a program with dagger() for uncomputation.

Solution:

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

def quantum_adder_2bit(a0, a1, b0, b1, carry):
    """Build a 2-bit ripple-carry adder circuit.
    Input:  a = (a1, a0), b = (b1, b0)
    Output: a + b stored in b qubits, carry in carry qubit
    """
    cir = QCircuit()
    # Half adder for bit 0
    cir << CNOT(a0, b0)
    # Full adder for bit 1 (simplified)
    cir << TOFFOLI(a0, b0, carry)
    cir << CNOT(a0, b1)
    cir << CNOT(b0, b1)
    cir << TOFFOLI(a1, b1, carry)
    return cir

qvm = CPUQVM()

# Test: 2 + 1 = 3 (a=10, b=01, result should be b=11, carry=0)
prog = QProg()
from pyqpanda3.core import X

# Encode a=2 (binary 10)
prog << X(1)  # a1=1
# Encode b=1 (binary 01)
prog << X(3)  # b0=1

# Apply adder
adder = quantum_adder_2bit(0, 1, 2, 3, 4)
prog << adder
prog << measure([2, 3, 4], [0, 1, 2])

qvm.run(prog, shots=100)
counts = qvm.result().get_counts()
print(f"2+1 result: {counts}")

# Uncompute by applying dagger
prog2 = QProg()
prog2 << X(1) << X(3)
prog2 << adder
prog2 << adder.dagger()  # Uncompute
prog2 << measure([2, 3, 4], [0, 1, 2])
qvm.run(prog2, shots=100)
counts2 = qvm.result().get_counts()
print(f"After uncompute: {counts2}")
# Should show original b values restored

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 CPUQVM, 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(circuit=cir)

# 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 = [str(dag.get_gate(idx)) for idx in layer]
    print(f"  Layer {i}: {gate_names}")

Released under the MIT License.