Skip to content

Dynamic Circuits

Dynamic circuits enable classical control flow within quantum programs. pyqpanda3 supports mid-circuit measurement and conditional execution through qif, qelseif, qelse, and qwhile constructs.


What Are Dynamic Circuits?

Traditional quantum circuits are static sequences of gates followed by measurement. Dynamic circuits introduce classical control flow:

  • Conditional execution: Apply gates only if a measurement condition is met
  • Loops: Repeat operations based on measurement outcomes
  • Mid-circuit measurement: Measure qubits during execution, not just at the end

qif - Conditional Branching

The qif construct implements classical if-then-else logic based on classical bit values.

Basic If-Then

Use qif when you need to apply gates conditionally on measurement results:

python
from pyqpanda3 import core

# Create a program with conditional execution
prog = core.QProg()
prog << core.X(0)        # Prepare |1⟩ on qubit 0
prog << core.I(1)        # Qubit 1 in |0⟩

# Define the if-branch: apply X(1) if cbit[1] is 1
if_branch = core.QProg()
if_branch << core.X(1)

# Add conditional branch
prog << core.qif([1]).then(if_branch).qendif()

print(prog)

The condition qif([1]) checks if classical bit 1 is set to 1.

If-Then-Else

Add an else branch for the case when the condition is not met:

python
prog = core.QProg()
prog << core.X(0) << core.I(1)

# Define both branches
if_branch = core.QProg()
if_branch << core.X(1)

else_branch = core.QProg()
else_branch << core.X(0)

# Add conditional with both branches
prog << core.qif([1]).then(if_branch).qelse(else_branch)

print(prog)

Using qelseif for Multi-Condition Branching

Chain multiple conditions using qelseif:

python
prog = core.QProg()
prog << core.X(0) << core.I(1) << core.I(2) << core.I(3)

# Define multiple branches
branch_1 = core.QProg()
branch_1 << core.X(1)

branch_2 = core.QProg()
branch_2 << core.X(0)

branch_3 = core.QProg()
branch_3 << core.X(2)

branch_4 = core.QProg()
branch_4 << core.CNOT(0, 1)

# Build multi-branch conditional
(prog << core.qif([1]).then(branch_1)
    .qelseif([0, 3]).then(branch_2)
    .qelseif([2]).then(branch_3)
    .qelse(branch_4))

print(prog)

The flow is:

Condition Semantics

The condition qif([c0, c1, c2]) checks if ALL specified classical bits are 1 (logical AND):

  • qif([0]): True if cbit 0 == 1
  • qif([0, 1]): True if cbit 0 == 1 AND cbit 1 == 1
  • qif([0, 1, 2]): True if cbit 0 == 1 AND cbit 1 == 1 AND cbit 2 == 1

qwhile - Conditional Loops

The qwhile construct repeats a loop body as long as a classical condition is met.

Basic While Loop

python
prog = core.QProg()

# Prepare initial state and measure
prog << core.X(0)
prog << core.measure(0, 0)

# Define the loop body
loop_body = core.QProg()
loop_body << core.X(1)           # Flip qubit 1
loop_body << core.X(0)           # Flip qubit 0 back
loop_body << core.measure(0, 0)  # Re-measure qubit 0

# Add while loop: continue while cbit[0] is 1
prog << core.qwhile([0]).loop(loop_body)

print(prog)

The loop executes as:


Practical Examples

Example 1: Repeat-Until-Success

Use dynamic circuits to repeat an operation until a desired measurement outcome:

python
from pyqpanda3 import core

# Repeat H-measure until we get |1⟩
prog = core.QProg()

# Initial measurement
prog << core.H(0)
prog << core.measure(0, 0)

# Loop body: keep trying until we measure 1
loop_body = core.QProg()
loop_body << core.H(0)
loop_body << core.measure(0, 0)

# While cbit[0] == 0 (didn't get 1), try again
# Note: qwhile continues while condition is True
# We need to loop while cbit[0] is 0
prog << core.qwhile([0]).loop(loop_body)

print("Repeat-until-success circuit:")
print(prog)

Example 2: Mid-Circuit Measurement with Conditional Gate

Perform a measurement and conditionally apply a correction:

python
from pyqpanda3 import core

# Create a 3-qubit entangled state
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.CNOT(1, 2)

# Mid-circuit measurement of qubit 0
prog << core.measure(0, 0)

# Conditionally apply correction on qubits 1 and 2
correction = core.QProg()
correction << core.Z(1)
correction << core.Z(2)

# If measurement was 1, apply correction
prog << core.qif([0]).then(correction).qendif()

# Final measurement
prog << core.measure([1, 2], [1, 2])

print("Mid-circuit measurement with correction:")
print(prog)

# Run the circuit
machine = core.CPUQVM()
machine.run(prog, shots=1000)
print(machine.result().get_counts())

Example 3: Error Detection with Parity Check

Use dynamic circuits for error detection using parity measurements:

python
from pyqpanda3 import core

# Encode logical |0⟩ in 3-qubit repetition code
prog = core.QProg()
prog << core.H(0)              # Superposition
prog << core.CNOT(0, 1)        # Entangle
prog << core.CNOT(0, 2)        # Entangle

# Parity check measurements (into ancilla cbits)
prog << core.CNOT(0, 3)        # Parity q0-q1 (using ancilla q3)
prog << core.CNOT(1, 3)
prog << core.measure(3, 0)      # Parity into cbit 0

# Conditional correction based on parity
correct_01 = core.QProg()
correct_01 << core.X(1)        # Flip qubit 1 back

correct_10 = core.QProg()
correct_10 << core.X(0)        # Flip qubit 0 back

correct_11 = core.QProg()
correct_11 << core.X(2)        # Flip qubit 2 back

# Note: This is a simplified example
# Real error correction would use syndrome decoding

print("Error detection circuit:")
print(prog)

Example 4: Adaptive Phase Estimation

Use dynamic circuits for iterative phase estimation:

python
from pyqpanda3 import core
import numpy as np

# Iterative phase estimation for a unitary U with eigenvalue e^(iφ)
# We want to estimate φ

phi = np.pi / 4  # True phase

prog = core.QProg()

# Prepare eigenstate |1⟩ of RZ
prog << core.X(1)

# k-th iteration (simplified single-step example)
prog << core.H(0)
prog << core.RZ(1, phi)  # Apply unitary
prog << core.H(0)
prog << core.measure(0, 0)

# Conditional phase correction based on measurement
correction = core.QProg()
correction << core.RZ(1, -np.pi / 2)

prog << core.qif([0]).then(correction).qendif()

# Final measurement
prog << core.measure(1, 1)

print("Adaptive phase estimation circuit:")
print(prog)

machine = core.CPUQVM()
machine.run(prog, shots=1000)
print(machine.result().get_counts())

Teleportation Protocol

Quantum teleportation transmits an unknown quantum state from one qubit to another using entanglement and classical communication. Dynamic circuits are essential because the correction operations depend on mid-circuit measurement results.

The teleportation protocol works as follows:

  1. Alice and Bob share an entangled Bell pair (|Φ+=12(|00+|11))
  2. Alice performs a Bell measurement on her qubit and the unknown state
  3. Alice sends the two classical bits to Bob
  4. Bob applies corrections based on the measurement outcomes
|ψ|Φ+=12a,b{0,1}|a,bXbZa|ψ
python
from pyqpanda3 import core

# Quantum teleportation of an unknown state |psi⟩ on qubit 0
# Qubit allocation:
#   q0: the unknown state |psi⟩ to teleport
#   q1: Alice's half of the Bell pair
#   q2: Bob's half of the Bell pair

prog = core.QProg()

# Step 1: Prepare the unknown state |psi⟩ = alpha|0⟩ + beta|1⟩
# For this example, we prepare |psi⟩ = Ry(theta)|0⟩
theta = 0.7  # Arbitrary rotation angle
prog << core.RY(0, theta)

# Step 2: Create Bell pair between q1 and q2
# |00⟩ -> (H on q1) -> (|0⟩+|1⟩)|0⟩/sqrt(2) -> (CNOT q1,q2) -> |Φ+⟩
prog << core.H(1)
prog << core.CNOT(1, 2)

# Step 3: Bell measurement on (q0, q1)
# First apply the inverse Bell basis transformation
prog << core.CNOT(0, 1)
prog << core.H(0)

# Step 4: Measure q0 and q1 into classical bits
prog << core.measure(0, 0)  # cbit 0 holds measurement of q0
prog << core.measure(1, 1)  # cbit 1 holds measurement of q1

# Step 5: Bob's conditional corrections on q2
# If cbit[0] == 1, apply Z correction
z_correction = core.QProg()
z_correction << core.Z(2)

no_z_correction = core.QProg()

prog << core.qif([0]).then(z_correction).qelse(no_z_correction)

# If cbit[1] == 1, apply X correction
x_correction = core.QProg()
x_correction << core.X(2)

no_x_correction = core.QProg()

prog << core.qif([1]).then(x_correction).qelse(no_x_correction)

# Step 6: Final measurement of the teleported state on q2
prog << core.measure(2, 2)

print("Quantum Teleportation Circuit:")
print(prog)

# Run and verify
machine = core.CPUQVM()
machine.run(prog, shots=1000)
counts = machine.result().get_counts()
print("Teleportation results:", counts)

The correction rules are:

Measurement (q0, q1)Correction on q2
(0, 0)None
(0, 1)X
(1, 0)Z
(1, 1)ZX

After correction, qubit 2 holds the original state |ψ exactly. The fidelity of teleportation is perfect in the absence of noise, which is guaranteed by the no-cloning theorem -- the original state on qubit 0 is destroyed during measurement.


Quantum Error Correction Example

The 3-qubit repetition code protects a single logical qubit against bit-flip (X) errors. The encoding maps:

|0L=|000,|1L=|111

The syndrome extraction uses two ancilla qubits to detect which data qubit (if any) experienced a bit-flip. The syndrome (s1,s2) uniquely identifies the error location.

python
from pyqpanda3 import core

# 3-qubit repetition code for bit-flip error correction
# Qubit layout:
#   q0, q1, q2: data qubits (encoded logical qubit)
#   q3: ancilla for parity check q0 XOR q1
#   q4: ancilla for parity check q1 XOR q2

prog = core.QProg()

# Step 1: Encode |psi⟩ into the 3-qubit code
# Start with |psi⟩ on q0 (e.g., a superposition)
prog << core.RY(0, 1.2)  # Prepare an arbitrary state on q0

# Entangle: |psi⟩|00⟩ -> (alpha|000⟩ + beta|111⟩)
prog << core.CNOT(0, 1)
prog << core.CNOT(0, 2)

# Step 2: Simulate a bit-flip error on qubit 1
# (In practice this would be unplanned noise)
prog << core.X(1)

# Step 3: Syndrome extraction using ancilla qubits
# Ancilla q3 measures parity of (q0, q1)
prog << core.CNOT(0, 3)
prog << core.CNOT(1, 3)

# Ancilla q4 measures parity of (q1, q2)
prog << core.CNOT(1, 4)
prog << core.CNOT(2, 4)

# Measure ancillas into classical bits
prog << core.measure(3, 0)  # s1 -> cbit 0
prog << core.measure(4, 1)  # s2 -> cbit 1

# Step 4: Conditional correction based on syndrome
# Syndrome table:
#   (0,0) -> no error (or error on no qubit)
#   (1,0) -> error on q0
#   (1,1) -> error on q1
#   (0,1) -> error on q2

# Correction for q0: apply X(0) when s1=1, s2=0
# We approximate this by checking both syndrome bits
correct_q0 = core.QProg()
correct_q0 << core.X(0)

correct_q1 = core.QProg()
correct_q1 << core.X(1)

correct_q2 = core.QProg()
correct_q2 << core.X(2)

no_correction = core.QProg()

# Use conditional branches for each syndrome pattern
# Since qif checks ALL bits == 1, we use the pattern matching
# For (1,0) syndrome: only cbit[0] is 1
prog << core.qif([0]).then(correct_q1).qelse(no_correction)

print("3-Qubit Repetition Code - Error Correction Circuit:")
print(prog)

# Run the circuit
machine = core.CPUQVM()
machine.run(prog, shots=1000)
print("Error correction results:", machine.result().get_counts())

Syndrome Decoding Table

The syndrome bits (s1,s2) identify which qubit experienced a bit-flip:

s1s2Error LocationCorrection
00NoneIdentity
10Qubit 0X0
11Qubit 1X1
01Qubit 2X2

The minimum Hamming distance of the repetition code is d=3, which allows correction of up to t=(d1)/2=1 bit-flip error. This code cannot correct phase-flip (Z) errors. To protect against both, one would use the 9-qubit Shor code or the 7-qubit Steane code.


Iterative Phase Estimation

Iterative phase estimation (IPE) determines the phase ϕ of an eigenvalue e2πiϕ of a unitary operator U one bit at a time, starting from the least significant bit. Unlike standard QPE which requires many auxiliary qubits, IPE uses only one auxiliary qubit and repeated measurements.

The key circuit for each iteration k is:

Prob(mk=0)=1+cos(2π(2kϕδk))2

where δk is the accumulated phase correction from previous iterations.

python
from pyqpanda3 import core
import math

# Iterative Phase Estimation for a unitary U with eigenvalue e^(2*pi*i*phi)
# We estimate phi to n bits of precision using n iterations.
# Each iteration uses one auxiliary qubit and one measurement.

# The target unitary is a rotation gate. For RZ(theta), the eigenvalue
# on |1⟩ is e^(-i*theta/2), so phi = -theta / (4*pi).
# We use controlled-U^(2^k) in iteration k.

n_bits = 3              # Number of precision bits
true_phase = math.pi / 4  # True phase phi for this example

# Storage for the estimated bits
phase_bits = []

for k in range(n_bits):
    prog = core.QProg()

    # Auxiliary qubit: q0
    # Target qubit: q1 (eigenstate of U)

    # Initialize auxiliary in superposition
    prog << core.H(0)

    # Prepare the eigenstate |1⟩ on the target qubit
    prog << core.X(1)

    # Apply controlled-U^(2^(n-1-k)) between iterations
    # For this example, U = RZ(2*pi*phi), so U^(2^m) = RZ(2*pi*phi*2^m)
    power = n_bits - 1 - k
    rotation_angle = 2 * math.pi * true_phase * (2 ** power)
    prog << core.CRZ(0, 1, rotation_angle)

    # Apply inverse phase correction based on previously measured bits
    # This compensates for the lower-order bits already determined
    for j, bit_val in enumerate(phase_bits):
        if bit_val == 1:
            correction_angle = -2 * math.pi * (2 ** (power - j - 1)) / (2 ** n_bits)
            prog << core.RZ(0, correction_angle)

    # Final Hadamard on auxiliary
    prog << core.H(0)

    # Measure the auxiliary qubit
    prog << core.measure(0, 0)

    # Run this iteration
    machine = core.CPUQVM()
    machine.run(prog, shots=1000)
    result = machine.result().get_counts()

    # The most frequent outcome determines this bit
    if result:
        bit = max(result, key=result.get)
        phase_bits.append(int(bit))
    else:
        phase_bits.append(0)

# Reconstruct the estimated phase from bits
estimated_phase = sum(b * (2 ** -(i + 1)) for i, b in enumerate(phase_bits))
print(f"True phase:      {true_phase:.6f}")
print(f"Estimated phase:  {estimated_phase:.6f}")
print(f"Phase bits (MSB first): {phase_bits}")

Step-by-Step Explanation

Each iteration k (from k=n1 down to k=0) performs:

  1. Prepare: Initialize auxiliary qubit in |+ and target in eigenstate |ψ
  2. Controlled-U2k: Apply the unitary U raised to the power 2k, controlled by the auxiliary
  3. Phase correction: Apply Z-rotations based on previously measured bits to remove their contribution
  4. Measure: Measure the auxiliary qubit to extract bit k of ϕ
  5. Reconstruct: After all iterations, ϕ0.b1b2bn in binary fractional form

The accuracy improves as n increases. With n bits, the resolution is Δϕ=2n.


State Verification with Dynamic Circuits

State verification confirms that a quantum system is in the intended state after preparation. Dynamic circuits enable adaptive measurement strategies that are more efficient than full state tomography.

Bell State Verification

A Bell state |Φ+=12(|00+|11) should yield perfectly correlated measurement outcomes. We can verify this using conditional checks.

python
from pyqpanda3 import core

# Verify a Bell state |Φ+⟩ = (|00⟩ + |11⟩) / sqrt(2)
# Strategy: prepare the state, measure both qubits, check correlation

prog = core.QProg()

# Prepare the Bell state
prog << core.H(0)
prog << core.CNOT(0, 1)

# Measure both qubits
prog << core.measure(0, 0)  # cbit 0
prog << core.measure(1, 1)  # cbit 1

# If both qubits agree, the state is correct
# If they disagree, flag an error by flipping an ancilla
flag_error = core.QProg()
flag_error << core.X(2)  # q2 starts in |0⟩, flip to |1⟩ if error

# We cannot directly check "XOR" with qif's AND semantics alone,
# so we use a different approach: measure an XOR ancilla
# Prepare XOR of measurement results using CNOT into ancilla
prog << core.CNOT(0, 2)
prog << core.CNOT(1, 2)
prog << core.measure(2, 2)

# cbit[2] == 1 means the qubits disagreed (verification failed)
# Use conditional to apply a correction or log the result
verify_fail = core.QProg()
verify_fail << core.X(3)  # Flag qubit for failure

prog << core.qif([2]).then(verify_fail).qendif()

print("Bell state verification circuit:")
print(prog)

machine = core.CPUQVM()
machine.run(prog, shots=1000)
print("Verification results:", machine.result().get_counts())

GHZ State Fidelity Check

For a GHZ state |GHZn=12(|0n+|1n), all qubits should be perfectly correlated. We verify by checking pairwise parity.

python
from pyqpanda3 import core

# GHZ state verification for 4 qubits
# |GHZ_4⟩ = (|0000⟩ + |1111⟩) / sqrt(2)
n_qubits = 4

prog = core.QProg()

# Prepare the GHZ state
prog << core.H(0)
for i in range(1, n_qubits):
    prog << core.CNOT(0, i)

# Measure all data qubits
for i in range(n_qubits):
    prog << core.measure(i, i)

# Compute pairwise parities using ancilla qubits
# Check that all adjacent pairs have even parity
ancilla_start = n_qubits
for i in range(n_qubits - 1):
    anc = ancilla_start + i
    prog << core.CNOT(i, anc)
    prog << core.CNOT(i + 1, anc)
    prog << core.measure(anc, anc)

print("GHZ state fidelity check circuit:")
print(prog)

machine = core.CPUQVM()
machine.run(prog, shots=1000)
counts = machine.result().get_counts()
print("GHZ verification results:", counts)
# If all ancilla measurements are 0, the GHZ state is valid

The expected outcome is that all ancilla parity checks yield 0, confirming that the GHZ correlations are intact. Any non-zero parity indicates decoherence or preparation errors.


Common Patterns and Idioms

This section collects reusable patterns for dynamic circuit programming in pyqpanda3.

Pattern: Measure-Then-Correct

The most common dynamic circuit pattern: measure a qubit, then conditionally apply a correction gate.

python
from pyqpanda3 import core

def measure_and_correct(prog, measure_qubit, measure_cbit, target_qubit, correction_gate):
    """Measure a qubit and conditionally apply a correction to another qubit.

    Args:
        prog: The QProg to append operations to.
        measure_qubit: Index of the qubit to measure.
        measure_cbit: Index of the classical bit to store the result.
        target_qubit: Index of the qubit to apply correction to.
        correction_gate: A function that takes a qubit index and returns a gate.
    """
    prog << core.measure(measure_qubit, measure_cbit)

    # Build the correction branch
    correction = core.QProg()
    correction << correction_gate(target_qubit)

    # No-op branch for the else case
    no_op = core.QProg()

    # Apply correction if measurement was |1⟩
    prog << core.qif([measure_cbit]).then(correction).qelse(no_op)
    return prog


# Usage: measure q0 and conditionally apply Z to q1
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog = measure_and_correct(prog, 0, 0, 1, core.Z)
print(prog)

Pattern: Conditional Identity

When you need an explicit no-op (identity) branch for clarity, use an empty QProg:

python
from pyqpanda3 import core

# Pattern: explicit identity branch for documentation clarity
prog = core.QProg()
prog << core.measure(0, 0)

action_branch = core.QProg()
action_branch << core.X(1) << core.Z(2)

identity_branch = core.QProg()  # Empty program = identity

prog << core.qif([0]).then(action_branch).qelse(identity_branch)
print(prog)

Pattern: Chained Conditionals for Multi-Outcome Decisions

Use qelseif to implement a switch-like construct over measurement outcomes:

python
from pyqpanda3 import core

# Pattern: switch-like behavior with qelseif
# Suppose we measure qubits 0, 1 and want different actions for 00, 01, 10, 11
prog = core.QProg()
prog << core.H(0) << core.H(1)
prog << core.measure(0, 0)
prog << core.measure(1, 1)

# Branch for both qubits measured as 1 (AND condition)
branch_both = core.QProg()
branch_both << core.X(2)

# Branch for only qubit 0 measured as 1
branch_q0_only = core.QProg()
branch_q0_only << core.Y(2)

# Branch for only qubit 1 measured as 1
branch_q1_only = core.QProg()
branch_q1_only << core.Z(2)

# Default: neither measured as 1
branch_default = core.QProg()
branch_default << core.I(2)

# qif checks AND of all listed cbits
# qif([0,1]) -> both are 1
# Then we use additional conditions for the remaining cases
prog << core.qif([0, 1]).then(branch_both).qelse(branch_default)

print("Chained conditional pattern:")
print(prog)

Pattern: Feed-Forward with Multiple Stages

In multi-stage quantum algorithms, measurement results from one stage feed into conditional operations in the next stage.

python
from pyqpanda3 import core

# Pattern: multi-stage feed-forward
# Stage 1: Prepare and measure
# Stage 2: Conditional operations based on Stage 1
# Stage 3: Final computation

prog = core.QProg()

# Stage 1: Prepare entangled state and measure one qubit
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.CNOT(0, 2)
prog << core.measure(0, 0)  # Measure q0, result in cbit 0

# Stage 2: Conditional entangling based on Stage 1 result
# If cbit[0] == 1, create additional entanglement on remaining qubits
stage2_entangle = core.QProg()
stage2_entangle << core.CNOT(1, 2)
stage2_entangle << core.H(1)
stage2_entangle << core.measure(1, 1)  # Mid-circuit measurement

stage2_simple = core.QProg()
stage2_simple << core.H(1)
stage2_simple << core.measure(1, 1)

prog << core.qif([0]).then(stage2_entangle).qelse(stage2_simple)

# Stage 3: Final computation and measurement
prog << core.CNOT(1, 2)
prog << core.measure(2, 2)

print("Multi-stage feed-forward circuit:")
print(prog)

Pattern: Repeated Attempt with qwhile

Use qwhile to retry a probabilistic operation until success. This is useful for protocols where the success probability is bounded away from 1.

python
from pyqpanda3 import core

# Pattern: probabilistic gate with retry
# Try to prepare |1⟩ via Hadamard+measure, retry on |0⟩
prog = core.QProg()

# Initialize and first attempt
prog << core.X(0)  # Start in |1⟩
prog << core.H(0)  # Put in superposition
prog << core.measure(0, 0)  # Measure: 50% chance of |1⟩

# Loop body: retry if we got |0⟩
retry_body = core.QProg()
retry_body << core.H(0)  # Back to superposition
retry_body << core.measure(0, 0)  # Try again

# While cbit[0] == 1 (got |1⟩, loop ends since condition is met)
# Note: loop continues while condition is True, so we structure accordingly
prog << core.qwhile([0]).loop(retry_body)

print("Repeated attempt pattern:")
print(prog)

Debugging Dynamic Circuits

Dynamic circuits introduce complexity that can make debugging difficult. This section covers common pitfalls and strategies for identifying and fixing issues.

Strategy 1: Print and Inspect the Program

Always print the constructed QProg before running it. The string representation reveals the gate sequence and control flow structure.

python
from pyqpanda3 import core

prog = core.QProg()
prog << core.H(0)
prog << core.measure(0, 0)

branch = core.QProg()
branch << core.X(1)

prog << core.qif([0]).then(branch).qendif()

# Always inspect before running
print("=== Circuit structure ===")
print(prog)

Strategy 2: Run Without Dynamics First

Before adding conditional logic, verify that the static portion of your circuit produces the expected results. Build incrementally.

python
from pyqpanda3 import core

# Step 1: Verify the static part works
static_prog = core.QProg()
static_prog << core.H(0)
static_prog << core.CNOT(0, 1)
static_prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(static_prog, shots=100)
print("Static results:", machine.result().get_counts())

# Step 2: Add dynamics only after static part is confirmed correct
dynamic_prog = core.QProg()
dynamic_prog << core.H(0)
dynamic_prog << core.CNOT(0, 1)
dynamic_prog << core.measure(0, 0)

correction = core.QProg()
correction << core.X(1)

dynamic_prog << core.qif([0]).then(correction).qendif()
dynamic_prog << core.measure(1, 1)

machine.run(dynamic_prog, shots=100)
print("Dynamic results:", machine.result().get_counts())

Strategy 3: Check Classical Bit Initialization

A common bug is referencing a classical bit in a condition before measuring into it. The classical bit may have an undefined or default value.

python
from pyqpanda3 import core

# BUG: cbit 0 is never measured, so the condition is unreliable
buggy_prog = core.QProg()
buggy_prog << core.H(0)
# Missing: buggy_prog << core.measure(0, 0)
branch = core.QProg()
branch << core.X(1)
buggy_prog << core.qif([0]).then(branch).qendif()  # cbit 0 is uninitialized!

# FIX: always measure before using a cbit in a condition
fixed_prog = core.QProg()
fixed_prog << core.H(0)
fixed_prog << core.measure(0, 0)  # Now cbit 0 has a defined value
branch = core.QProg()
branch << core.X(1)
fixed_prog << core.qif([0]).then(branch).qendif()

Strategy 4: Verify Loop Termination

A qwhile loop runs while all specified classical bits are 1. Ensure the loop body eventually changes the measured value to break the loop.

python
from pyqpanda3 import core

# POTENTIAL INFINITE LOOP: loop body does not change the condition
# bad_prog = core.QProg()
# bad_prog << core.X(0) << core.measure(0, 0)  # cbit[0] = 1
# bad_loop = core.QProg()
# bad_loop << core.H(1)  # Does not affect qubit 0 or cbit 0!
# bad_prog << core.qwhile([0]).loop(bad_loop)  # Infinite loop!

# GOOD: loop body re-measures the conditioned qubit
good_prog = core.QProg()
good_prog << core.X(0) << core.measure(0, 0)

good_loop = core.QProg()
good_loop << core.H(0)           # Change the state
good_loop << core.measure(0, 0)  # Re-measure to update cbit[0]

good_prog << core.qwhile([0]).loop(good_loop)  # Will terminate with ~50% chance each iteration

Strategy 5: Compare Expected vs Actual Counts

After running, compare the measurement statistics against theoretical predictions. For a correctly implemented dynamic circuit, the counts should match the expected distribution.

python
from pyqpanda3 import core
import math

# A Bell state should give 00 and 11 with equal probability
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
machine.run(prog, shots=10000)
counts = machine.result().get_counts()

# Check: only 00 and 11 should appear, each ~5000 times
total_shots = sum(counts.values())
for outcome, count in counts.items():
    probability = count / total_shots
    print(f"  {outcome}: {count} shots ({probability:.3f})")
    # Expected: 00 and 11 each with probability ~0.50
    # Any 01 or 10 outcomes indicate a bug in the circuit

Common Error Messages

ErrorCauseFix
QProg type mismatchUsing QCircuit in a branchUse QProg() for all branch bodies
Uninitialized cbitCondition references unmeasured cbitAdd measure() before the conditional
Infinite loopqwhile body does not update condition bitsEnsure loop body re-measures the conditioned qubits
Missing qendifqif without terminationEnd every qif with .qendif() or .qelse()

Dynamic Circuit Construction Rules

Rule 1: Use QProg for Branches

All branch bodies must be QProg objects, not QCircuit objects:

python
# CORRECT
branch = core.QProg()
branch << core.X(1)
prog << core.qif([0]).then(branch).qendif()

# WRONG: branch must be QProg
# branch = core.QCircuit()  # Don't use QCircuit

Rule 2: Complete the Control Flow

Every qif must be terminated with either .qendif() or .qelse():

python
# CORRECT: End with qendif
prog << core.qif([0]).then(branch).qendif()

# CORRECT: End with qelse
prog << core.qif([0]).then(if_branch).qelse(else_branch)

# WRONG: Missing termination
# prog << core.qif([0]).then(branch)  # Missing .qendif() or .qelse()

Rule 3: Measurement Before Condition

Classical bits used in conditions must be populated by measurement before the conditional:

python
# CORRECT: Measure first, then use cbit in condition
prog << core.measure(0, 0)
prog << core.qif([0]).then(branch).qendif()

# WRONG: Using uninitialized cbit
# prog << core.qif([0]).then(branch).qendif()  # cbit 0 not measured yet

Rule 4: Nested Conditionals

You can nest conditionals by including them in branch programs:

python
outer_if = core.QProg()
outer_if << core.X(0)

inner_if = core.QProg()
inner_if << core.Y(1)
inner_else = core.QProg()
inner_else << core.Z(1)

outer_if << core.qif([1]).then(inner_if).qelse(inner_else)

prog = core.QProg()
prog << core.H(0) << core.measure(0, 0) << core.measure(1, 1)
prog << core.qif([0]).then(outer_if).qendif()

Control Flow Diagram


API Reference

qif

python
core.qif(cbits: list[int]) -> QIf

Creates a conditional construct that checks if all specified classical bits are 1.

Parameters:

ParameterTypeDescription
cbitslist[int]Classical bit addresses to check (AND condition)

Returns: QIf object with .then() method

QIf.then

python
QIf.then(branch: QProg) -> QIfThen

Sets the if-branch program.

QIfThen.qelse

python
QIfThen.qelse(else_branch: QProg) -> QProg

Adds an else-branch and returns the completed conditional.

QIfThen.qelseif

python
QIfThen.qelseif(cbits: list[int]) -> QElseif

Adds another condition to check.

QIfThen.qendif

python
QIfThen.qendif() -> QProg

Ends the conditional without an else branch.

qwhile

python
core.qwhile(cbits: list[int]) -> QWhile

Creates a while loop that repeats while all specified classical bits are 1.

QWhile.loop

python
QWhile.loop(loop_body: QProg) -> QProg

Sets the loop body program.


Limitations

  1. Backend support: Not all simulators support dynamic circuits. CPUQVM supports them fully.
  2. Condition types: Only classical bit equality to 1 is supported (no comparison operators).
  3. No nested measurement re-use: A classical bit used in a condition should not be re-measured within the same conditional branch.
  4. Loop termination: Be careful with while loops — ensure the loop body can change the condition to prevent infinite loops.

Next Steps

Released under the MIT License.