I was always some way skeptical with using AI (LLMs in this case) to create code. But don’t get me wrong, I love AI and always have the urge to use it more frequently, I just can’t afford for better models such as claude or newer versions of GPT, neither enough hardware to run GLM or Kimi.

Recently, I saw the last LiveOverflow video and it hit me in a different way. The way Fabian showed his urge to explore LLMs made me want too. I recommend deeply you guys to watch this video yourselves.

LiveOverflow Video
LiveOverflow Video

Anyway, I’m not very much into security, even though it’s another topic I find really intesresting, I’m still a courious guy in the field of research, mostly in AI and quantum computing.

Nowdays, I’m facing big problems with my QCOP project. At first, I used this code to generate my dataset:

"""Generate random circuits by hand"""

import random
from math import pi
from abc import ABC, abstractmethod

from qiskit import QuantumCircuit
from qiskit.circuit import Gate as QiskitGate
from qiskit.circuit.library import (
    ZGate,
    XGate,
    HGate,
    RYGate,
    CXGate,
    CZGate,
    IGate
)

class Gate(ABC):
    """interface for gates"""

    @classmethod
    @abstractmethod
    def get_random_gate(cls) -> QiskitGate:
        pass


class SingleQubitGate(Gate):
    """Handle single qubit gates"""

    rotation_gates = [RYGate]
    simple_gates = [ZGate, XGate, HGate, IGate]
    all_gates = [*rotation_gates, *simple_gates]

    @classmethod
    def get_random_gate(cls) -> QiskitGate:
        """Return an instance of a single qubit gate ready to use"""
        gate = random.choice(cls.all_gates)

        if gate in cls.rotation_gates:
            param = random.uniform(0, 2 * pi)
            return gate(param)

        return gate()


class MultiQubitGate(Gate):
    """Handle multi qubit gates"""

    gates = [CXGate, CZGate]

    @classmethod
    def get_random_gate(cls) -> QiskitGate:
        """Return an instance of a multi-qubit gate ready to use"""
        gate = random.choice(cls.gates)
        return gate()

def generate_circuit(n_qubits:int, total_gates:int) -> QuantumCircuit:
    """Generate a circuit based on the amount of gates"""
    qc = QuantumCircuit(n_qubits)

    for _ in range(total_gates):
        if random.random() < 0.5:
            qubit = [random.randint(0, n_qubits - 1)]
            gate = SingleQubitGate.get_random_gate()
            qc.append(gate, qubit)
        else:
            qubits = random.sample(range(n_qubits), 2)
            gate = MultiQubitGate.get_random_gate()
            qc.append(gate, qubits)

        if random.random() < 0.1:
            qc.barrier()

    return qc

def get_random_circuit(n_qubits: int, total_gates: int) -> QuantumCircuit:
    """Generate a random circuit based on the amount of qubits and gates."""

    total_gates = random.randint(0, total_gates)
    return generate_circuit(n_qubits,total_gates)

Which generated pretty different circuits, But I had some issues with the circuit results. The circuit images were different, but the quantum states they generate was almost identical to each other. This way, the model was converging pretty fast, but overfitting since he would understand that every circuit has pretty much the same quantum state.

I was really pissed off by this code. I tried changing some factors, but it was not enough.

At the end, I tried using the built-in random circuit functions from qiskit. But the code was not what I needed. Currently, it has not enough tweaks that would allow me to generate the circuits I wanted to.

So, I got the inspiration from the source code, and started modifying it as I wished, adding barriers, the correct gates, changing probability and the general code structure. The final code was something like:

"""Generate random circuits by hand"""

from typing import List

from qiskit import QuantumCircuit
from qiskit.circuit.library import RYGate, XGate, ZGate, HGate, IGate, CXGate, CZGate, SwapGate

import numpy as np

from utils.constants import DEFAULT_RANDOM_SEED

class RandomCircuit:
    """Code based on https://github.com/Qiskit/qiskit/blob/stable/2.5/qiskit/circuit/random/utils.py#L685-L753"""

    def __init__(self, seed:int=37):
        self._all_gates = {
            'x': lambda  : XGate(),
            'z': lambda : ZGate(),
            'h': lambda : HGate(),
            'id': lambda : IGate(),
            'cx': lambda : CXGate(),
            'cz': lambda : CZGate(),
            'swap': lambda : SwapGate(),
            'ry':lambda theta: RYGate(theta)
        }
        self._two_qubit = ['cx', 'cz', 'swap']
        self._with_parameters = ['ry']

        self._low_param = 0
        self._high_param = 2*np.pi

        self._rng = np.random.default_rng(seed)

    def _get_angle(self) -> float:
        return self._rng.uniform(low=self._low_param,high=self._high_param,size=None)

    def _gate_num_qubits(self, gate:str) -> int:
        return int(gate in self._two_qubit)+1

    def _get_random_gates(self,num_gates:int)-> list[str]:
        return self._rng.choice(list(self._all_gates.keys()), num_gates)

    def _add_to_circuit(self, gates:List[str], qc:QuantumCircuit) -> None:
        num_qubits = qc.num_qubits
        for gate in gates:
            pos = self._rng.choice(range(num_qubits), self._gate_num_qubits(gate), replace=False).tolist()
            qc.append(
                self._all_gates[gate]()
                if gate not in self._with_parameters
                else self._all_gates[gate](self._get_angle())
            , pos, copy=False)

    def _add_barrier_at_the_end(self,qc:QuantumCircuit) -> None:
        if self._rng.random() <= 0.3:
            qc.barrier()

    def get_random_circuit(self, num_gates:int, num_qubits:int, add_barrier:bool=True, max_layers:int=5, min_layers:int=0) -> QuantumCircuit:
        qc = QuantumCircuit(num_qubits)

        gates = self._get_random_gates(num_gates)
        num_layers = self._rng.integers(min_layers,max_layers,1)[0]

        if not num_layers:
            self._add_to_circuit(gates, qc)
            self._add_barrier_at_the_end(qc)

        else:
            c = num_gates

            for _ in range(num_layers):
                layer_gates = self._rng.integers(0,c,1)[0]
                if not layer_gates:
                    continue

                selected_gates = self._rng.choice(gates,layer_gates,replace=False).tolist()
                self._add_to_circuit(selected_gates, qc)
                c -= len(selected_gates)

                if c <= 0:
                    self._add_barrier_at_the_end(qc)
                    break

                qc.barrier()

            if c > 0:
                selected_gates = self._rng.choice(gates,c,replace=False).tolist()
                self._add_to_circuit(selected_gates, qc)
                self._add_barrier_at_the_end(qc)


        return qc



def get_random_circuit(n_qubits: int, total_gates: int) -> QuantumCircuit:
    """Generate a random circuit based on the amount of qubits and gates."""
    rc = RandomCircuit(seed=DEFAULT_RANDOM_SEED)
    total_gates = np.random.randint(0, total_gates)
    return rc.get_random_circuit(total_gates, n_qubits, max_layers=total_gates)

However, another problem appeard. The generated circuits were not sparse enough. Most part of them would lead to the same results. I did a small experiment to identify how bad it was, and:

Amount of duplicated images from previous circuit generation algorithm
Amount of duplicated images from previous circuit generation algorithm

Well, from $60140$ images, only $620$ were unique. That’s pretty much $1\text{\%}$ of all images. Which is really bad.

The light

After watch the LiveOverflow video, I decided to use more AI to help me in my daily life.

I gave a prompt to gemini, and right in the first show It generated the code:

from typing import List, Optional
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit.library import RYGate, XGate, ZGate, HGate, IGate, CXGate, CZGate, SwapGate

class EnhancedRandomCircuit:
    def __init__(self, seed: Optional[int] = None):
        self._all_gates = {
            'x': lambda: XGate(),
            'z': lambda: ZGate(),
            'h': lambda: HGate(),
            'id': lambda: IGate(),
            'cx': lambda: CXGate(),
            'cz': lambda: CZGate(),
            'swap': lambda: SwapGate(),
            'ry': lambda theta: RYGate(theta)
        }
        self._single_qubit = ['x', 'z', 'h', 'id', 'ry']
        self._two_qubit = ['cx', 'cz', 'swap']
        self._with_parameters = ['ry']

        self._rng = np.random.default_rng(seed)

    def _get_angle(self) -> float:
        return float(self._rng.uniform(0, 2 * np.pi))

    def _select_target_qubits(self, num_qubits: int, num_targets: int, topology: str) -> List[int]:
        """Selects qubit targets based on sampled connectivity topology."""
        if num_targets == 1 or topology == "global" or num_qubits <= 2:
            return self._rng.choice(num_qubits, num_targets, replace=False).tolist()

        # Local / Nearest-Neighbor topology (creates sparse entanglement graphs)
        if topology == "local":
            q1 = int(self._rng.choice(num_qubits))
            # Choose adjacent qubit in a ring topology
            q2 = (q1 + int(self._rng.choice([-1, 1]))) % num_qubits
            return [q1, q2]
        
        return self._rng.choice(num_qubits, num_targets, replace=False).tolist()

    def get_random_circuit(
        self, 
        num_gates: int, 
        num_qubits: int, 
        max_layers: int = 5
    ) -> QuantumCircuit:
        qc = QuantumCircuit(num_qubits)

        # 1. Sample macro-style for this specific circuit instance
        p_two_qubit = self._rng.beta(0.5, 1.5)        # Biased toward single-qubit or two-qubit
        p_identity = self._rng.choice([0.0, 0.1, 0.3, 0.5]) # Forces active vs highly sparse qubits
        topology = self._rng.choice(["global", "local", "local"]) # Biased toward sparse spatial connectivity

        # 2. Build conditional gate probability distribution
        single_weight = (1.0 - p_two_qubit) * (1.0 - p_identity) / (len(self._single_qubit) - 1)
        
        weights = {}
        for g in self._single_qubit:
            weights[g] = p_identity if g == 'id' else single_weight
        for g in self._two_qubit:
            weights[g] = p_two_qubit / len(self._two_qubit)

        gate_names = list(weights.keys())
        gate_probs = np.array(list(weights.values()), dtype=np.float64)
        gate_probs /= np.sum(gate_probs)  # Normalize

        # 3. Distribute gates non-uniformly across layers
        num_layers = int(self._rng.integers(1, max_layers + 1))
        
        # Dirichlet distribution for non-uniform gate density per layer
        layer_splits = self._rng.dirichlet(np.ones(num_layers) * 0.5)
        gates_per_layer = self._rng.multinomial(num_gates, layer_splits)

        for l_idx, count in enumerate(gates_per_layer):
            if count == 0:
                continue

            selected_gates = self._rng.choice(gate_names, size=count, p=gate_probs)

            for g_name in selected_gates:
                n_q = 2 if g_name in self._two_qubit else 1
                pos = self._select_target_qubits(num_qubits, n_q, topology)

                gate_obj = (
                    self._all_gates[g_name](self._get_angle()) 
                    if g_name in self._with_parameters 
                    else self._all_gates[g_name]()
                )
                qc.append(gate_obj, pos, copy=False)

            if l_idx < num_layers - 1 and self._rng.random() < 0.4:
                qc.barrier()

        return qc

def get_random_circuit(n_qubits: int, total_gates: int) -> QuantumCircuit:
    """Thread-safe dataset generation function."""
    # Seed locally per invocation to avoid thread lock overhead while preserving thread-safety
    local_seed = np.random.randint(0, 2**31 - 1)
    rc = EnhancedRandomCircuit(seed=local_seed)

    # Vary max gates dynamically (from lightweight to full depth)
    gates = int(np.random.randint(1, max(2, total_gates + 1)))
    layers = int(np.random.randint(1, 8))

    return rc.get_random_circuit(num_gates=gates, num_qubits=n_qubits, max_layers=layers)

I used a jupyter notebook to examine the overall distribution, and look at this:

Gemini code results
Gemini code results

For $10000$ circuit approximatelly $98\text{\%}$ were unique, and for $30000$ $\approx 97\text{\%}$. THAT’S AMAZING!!!!!

Dancing

Now we can generate way better circuits.

I let it run sometime and at the end it generated: $60233$ unique circuits. That’s approximatelly a $97\text{\%}$ improvement overall.

I also ensured that the outcomes were sparse as possible:

the diversity of bitstrings in superposition
The Diversity of BitStrings in superposition

I also cleaned a bit the equal superposition, just as a way to avoid overfitting.

Final DataFrame size
Final Data Frame Size

Conclusion

Well, so far so good! Now it’s time to train the model and check the results.

I’ll be right back soon!

For interested people in the dataset, it’s available in its raw version at: https://huggingface.co/datasets/Dpbm/quantum-circuits and https://www.kaggle.com/datasets/dpbmanalysis/quantum-circuit-images.