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
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:
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:
# 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:
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: 2depth()
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:
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: 1count_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:
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:
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 removedto_circuit()
Convert the program to a QCircuit object for use with analysis tools that expect a circuit:
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:
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:
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:
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:
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
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()
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: 2qubits()
Return the set of qubit indices used by the circuit:
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()
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: 2dagger()
Return the adjoint (conjugate transpose) of the circuit. This reverses the gate order and replaces each gate with its dagger -- useful for uncomputation:
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:
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:
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
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:
set_name(), draw(), and originir()
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:
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 operand | Example | Description |
|---|---|---|
QGate | prog << core.H(0) | Append a single quantum gate |
QCircuit | prog << circuit | Append all gates from a circuit |
QProg | prog << other_prog | Absorb another program's operations |
| Measure result | prog << core.measure([0], [0]) | Append measurement operations |
3.2 Appending Gates
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
# 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:
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
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
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:
4.2 Two-Qubit Gates
Non-parameterized
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
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 rotationThe two-qubit rotation gates:
4.3 Multi-Qubit Gates
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:
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:
echo_gate = core.ECHO(0)
echo_gate.set_parameters([1.5]) # set custom parameterOracle constructs a quantum oracle from a truth table or binary function:
oracle = core.Oracle(qubits, truth_table)create_gate() constructs a gate from its type enum and parameters:
gate = core.create_gate("H", [0], []) # H gate on qubit 0
gate = core.create_gate("RX", [0], [1.57]) # RX gate with angle4.5 Gate Introspection and Transformations
Every gate object supports introspection:
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():
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
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
# 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
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
# 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
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
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
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
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
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
| Method | Returns | Description |
|---|---|---|
core.QProg() / core.QProg(n) | QProg | Create empty or pre-allocated program |
prog << x | QProg | Append gate, circuit, program, or measurement |
prog.qubits_num() | int | Number of qubits used |
prog.cbits_num() | int | Number of classical bits used |
prog.depth(only_q2) | int | Circuit 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() | QProg | Flatten all nested sub-circuits |
prog.to_circuit() | QCircuit | Convert to QCircuit |
prog.gate_operations() | list | List of gate operations |
prog.operations() | list | List of all operations |
prog.get_measure_nodes() | list | Extract measurement nodes |
prog.remap(mapping) | QProg | Remap qubit indices |
prog.originir() | str | Serialize to OriginIR string |
QCircuit
| Method | Returns | Description |
|---|---|---|
core.QCircuit() / core.QCircuit(n) | QCircuit | Create empty or pre-allocated circuit |
circ << x / circ.append(gate) | QCircuit | Append gate, circuit, or program |
circ.size() | int | Total gate count |
circ.depth() | int | Circuit depth |
circ.qubits() | set | Set of qubit indices used |
circ.count_ops(only_q2) | dict[str, int] | Gate count by name (optionally only 2-qubit) |
circ.num_2q_gate() | int | Number of two-qubit gates |
circ.dagger() | QCircuit | Adjoint (inverse) circuit |
circ.control(qubits) | QCircuit | Controlled version of circuit |
circ.expand() | QCircuit | Expand nested sub-circuits |
circ.matrix() | ndarray | Unitary matrix representation |
circ.set_name(name) | None | Set a human-readable name |
circ.originir() | str | Serialize to OriginIR |
DAGQCircuit
| Method | Returns | Description |
|---|---|---|
dag.from_circuit(circ) | None | Load gates from a QCircuit |
dag.build() | None | Construct the DAG edges |
dag.gates() | list | All gate objects |
dag.nodes() / dag.edges() | list | Node indices / edge pairs |
dag.get_gate(idx) | QGate | Gate at node index |
dag.layers() | iterator | Gate layers (parallel groups) |
dag.get_depth() | int | DAG depth (number of layers) |
dag.longest_path() | list | Critical path through DAG |
dag.two_qubit_gates() | list | All two-qubit gate objects |
dag.two_qubit_gate_nodes() | list | Node indices of 2Q gates |
Gate Factory Functions
| Category | Gates |
|---|---|
| 1Q non-parameterized | H, X, Y, Z, S, T, I, X1, Y1, Z1, ECHO |
| 1Q parameterized | RX, RY, RZ, P, RPhi, U1, U2, U3, U4 |
| 2Q non-parameterized | CNOT, CZ, SWAP, ISWAP, SQISWAP, MS |
| 2Q parameterized | CP, CR, CRX, CRY, CRZ, CU, RXX, RYY, RZZ, RZX |
| Multi-qubit | TOFFOLI |
| Special | BARRIER, ECHO, Oracle, create_gate() |
Summary
In this tutorial you learned:
QProg is the executable quantum program container. Use
<<to append gates, circuits, measurements, and other programs. Inspect it withdepth(),count_ops(),qubits_num(), and more.QCircuit is a reusable circuit module. It supports the same
<<operator plus additional methods likedagger(),control(),matrix(), andnum_2q_gate().The
<<operator is the universal append mechanism. It returns the container, enabling fluent chains likeprog << core.H(0) << core.CNOT(0, 1).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 likeBARRIERandOracle.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 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 (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:
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 restoredExercise 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:
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}")