Skip to content

Random Circuit (benchmarking & fuzz-testing)

Sometimes the circuit itself doesn't matter -- only that it's a real, valid one, for timing a backend or fuzz-testing a parser against whatever gate combinations show up. random_circuit fills that role, the same one Qiskit's own random_circuit plays.

Step 1. A reproducible random circuit

import dense_evolution as de

ops = de.random_circuit(n_qubits=3, n_gates=6, seed=0)
ops
[('t', 0), ('cx', 0, 2), ('t', 1), ('tdg', 1), ('z', 2), ('cz', 2, 1)]

random_circuit(n_qubits, n_gates, seed=None) returns a plain gate-tuple list, the same shape run_circuit_jit expects everywhere else in this package -- seed=0 makes the draw reproducible (the exact 6 gates above, every time); omit it for a fresh random circuit each call. two_qubit_prob (default 0.4) controls roughly what fraction of gates are two-qubit rather than single-qubit; gate_set restricts which named gates are eligible to be drawn, if only a subset should show up.

Step 2. It's a real circuit -- run it

sim = de.DenseSVSimulator(3)
sim.run_circuit_jit(ops)
sim.get_probabilities().round(4)
array([1., 0., 0., 0., 0., 0., 0., 0.])

Every gate random_circuit draws is real and runs exactly like a hand-written one -- here the whole probability mass lands back on |000> (this particular random draw happens to be entirely Z-basis-diagonal gates plus phase gates starting from |000>, so no amplitude ever moves off the all-zero state). A different seed, or more/ different gates, would spread probability across other basis states instead -- the draw is real, not guaranteed to look "interesting" every time.


Details

What it's for: benchmarking (a real circuit shape to time a backend against, without hand-writing one for every qubit count/depth combination) and fuzz-testing (a parser or compiler seeing gate combinations a human wouldn't necessarily think to write by hand).

random_circuit

Backward-compatibility shim -- the real implementation moved to dense_evolution.circuits.random_circuit as part of fixing the module/ function name collision (dense_evolution.qft/random_circuit each had a function sharing its own module's name -- any code importing the flat submodule path directly clobbered the package's re-exported function attribute with the module object, e.g. TypeError: 'module' object is not callable; see prog.txt). Kept so from dense_evolution.random_circuit import random_circuit (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working unchanged. Import from dense_evolution.circuits.random_circuit directly in new code.

random_circuit

random_circuit(
    n_qubits,
    n_gates,
    seed=None,
    gate_set=None,
    two_qubit_prob=0.4,
)

Build a random circuit for benchmarking or fuzz-testing.

Parameters:

Name Type Description Default
n_qubits int

Number of qubits, must be >= 1.

required
n_gates int

Number of gate operations to generate, must be >= 0.

required
seed int | Generator

Seed (or an existing Generator) for reproducible circuits.

None
gate_set iterable of str

Restrict generation to this set of gate names, mixing 1- and 2-qubit gates freely (default spans both: h, x, y, z, s, sdg, t, tdg, sx, rx, ry, rz, cx, cz, cy, swap). Unrecognized names raise immediately rather than failing later inside run_circuit.

None
two_qubit_prob float

Probability in [0, 1] of picking a 2-qubit gate at each step. Ignored (forced to single-qubit gates) once n_qubits < 2, or once gate_set excludes every 2-qubit gate name.

0.4

Returns:

Type Description
list[tuple]
Source code in dense_evolution/circuits/random_circuit.py
def random_circuit(n_qubits, n_gates, seed=None, gate_set=None, two_qubit_prob=0.4):
    """
    Build a random circuit for benchmarking or fuzz-testing.

    Parameters
    ----------
    n_qubits : int
        Number of qubits, must be >= 1.
    n_gates : int
        Number of gate operations to generate, must be >= 0.
    seed : int | numpy.random.Generator, optional
        Seed (or an existing Generator) for reproducible circuits.
    gate_set : iterable of str, optional
        Restrict generation to this set of gate names, mixing 1- and
        2-qubit gates freely (default spans both: h, x, y, z, s, sdg, t,
        tdg, sx, rx, ry, rz, cx, cz, cy, swap). Unrecognized names raise
        immediately rather than failing later inside run_circuit.
    two_qubit_prob : float
        Probability in [0, 1] of picking a 2-qubit gate at each step.
        Ignored (forced to single-qubit gates) once n_qubits < 2, or once
        gate_set excludes every 2-qubit gate name.

    Returns
    -------
    list[tuple]
    """
    if n_qubits < 1:
        raise ValueError(f"random_circuit needs at least 1 qubit, got {n_qubits}")
    if n_gates < 0:
        raise ValueError(f"n_gates must be >= 0, got {n_gates}")
    if not (0.0 <= two_qubit_prob <= 1.0):
        raise ValueError(f"two_qubit_prob must be in [0, 1], got {two_qubit_prob}")

    rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed)

    if gate_set is not None:
        gate_set = list(gate_set)
        unknown = set(gate_set) - _ALL_KNOWN
        if unknown:
            raise ValueError(f"unknown gate name(s) in gate_set: {sorted(unknown)}")
        one_q_static = [g for g in gate_set if g in _1Q_STATIC]
        one_q_param = [g for g in gate_set if g in _1Q_PARAMETRIC]
        two_q = [g for g in gate_set if g in _2Q_STATIC]
    else:
        one_q_static = list(_1Q_STATIC)
        one_q_param = list(_1Q_PARAMETRIC)
        two_q = list(_2Q_STATIC) if n_qubits >= 2 else []

    one_q_pool = one_q_static + one_q_param
    if not one_q_pool and not two_q:
        raise ValueError("gate_set leaves no usable gates for this n_qubits")

    ops = []
    for _ in range(n_gates):
        use_two_qubit = (
            bool(two_q) and n_qubits >= 2
            and (not one_q_pool or rng.random() < two_qubit_prob)
        )
        if use_two_qubit:
            name = two_q[rng.integers(len(two_q))]
            a, b = rng.choice(n_qubits, size=2, replace=False)
            ops.append((name, int(a), int(b)))
        else:
            name = one_q_pool[rng.integers(len(one_q_pool))]
            q = int(rng.integers(n_qubits))
            if name in _1Q_PARAMETRIC:
                theta = float(rng.uniform(0, 2 * np.pi))
                ops.append((name, q, theta))
            else:
                ops.append((name, q))
    return ops