Building a Quantum Random Number Generator with Qiskit

Table of Contents

  1. Why Build a Quantum RNG?
  2. Prerequisites
  3. Part 1: The Simplest QRNG (1 Qubit)
  4. Part 2: Multi-Qubit QRNG (8 Bits per Shot)
  5. Part 3: Entangled QRNG (Our Approach)
  6. Part 4: Running on Real Quantum Hardware
  7. Part 5: Converting Measurements to Usable Data
  8. Part 6: Verifying Randomness Quality
  9. Complete Code

Why Build a Quantum Random Number Generator?

Every random number your computer has ever produced is fake.

That sounds dramatic, but it is technically true. Classical computers are deterministic machines. They cannot produce true randomness — only pseudorandomness, sequences that look random but are completely determined by an initial seed. Give random.seed(42) to Python and it will produce the same "random" numbers every time, on every machine, forever.

For most applications, pseudorandomness is fine. But for cryptography, scientific simulation, and — as we discovered — generative art, there are good reasons to want the real thing.

A quantum random number generator (QRNG) exploits the fundamental indeterminacy of quantum mechanics to produce numbers that are genuinely, provably unpredictable. Not because the algorithm is complex. Because the universe itself has not decided the outcome until the moment of measurement.

A QRNG does not simulate randomness. It harvests randomness directly from quantum physics.

In this tutorial, you will build a working QRNG in Python using Qiskit and IBM Quantum's free cloud access. By the end, you will have code that runs on a real quantum computer and produces true random numbers you can use for anything.

This is the same approach we used to generate the seeds for all 100 NFTs in the Quantum Genesis collection.

Prerequisites

You will need:

  • Python 3.9+ installed on your machine
  • A free IBM Quantum account — sign up at quantum.ibm.com
  • Your IBM Quantum API token (found in your account settings)

Install the required packages:

pip install qiskit qiskit-ibm-runtime

That is it. No special hardware needed. IBM provides free access to real quantum processors with up to 156 qubits.

Part 1: The Simplest QRNG — One Qubit, One Bit

Let us start with the absolute simplest quantum random number generator: a single qubit producing a single random bit.

Step 1: Create the circuit

from qiskit.circuit import QuantumCircuit

# Create a circuit with 1 qubit and 1 classical bit
qc = QuantumCircuit(1, 1)

# Apply Hadamard gate: puts qubit in equal superposition of |0⟩ and |1⟩
qc.h(0)

# Measure the qubit into the classical bit
qc.measure(0, 0)

print(qc.draw())

Output:

     ┌───┐┌─┐
q_0: ┤ H ├┤M├
     └───┘└╥┘
c_0: ══════╩═

That is the entire circuit. Here is what happens:

  1. The qubit starts in state |0⟩ (the default)
  2. The Hadamard gate (H) puts it into an equal superposition: 50% chance of |0⟩, 50% chance of |1⟩
  3. Measurement forces the qubit to "choose" — and this choice is genuinely random according to quantum mechanics

The result is a single random bit: 0 or 1, with exactly 50/50 probability. No seed, no algorithm, no determinism. Pure quantum randomness.

Step 2: Run it on a simulator (for testing)

from qiskit.primitives import StatevectorSampler

sampler = StatevectorSampler()
job = sampler.run([qc], shots=10)
result = job.result()

# Get the counts
counts = result[0].data.c.get_counts()
print(counts)  # e.g., {'0': 6, '1': 4}

With 10 shots, you get 10 random bits. The simulator produces mathematically ideal results — but on real hardware, quantum noise adds genuine physical randomness.

Part 2: Multi-Qubit QRNG — 8 Bits per Shot

One bit at a time is slow. Let us scale up to 8 qubits, producing a full random byte with each measurement.

from qiskit.circuit import QuantumCircuit

def build_byte_circuit():
    """Create an 8-qubit circuit that produces one random byte per shot."""
    qc = QuantumCircuit(8, 8)

    # Apply Hadamard to all 8 qubits
    for i in range(8):
        qc.h(i)

    # Measure all qubits
    for i in range(8):
        qc.measure(i, i)

    return qc

qc = build_byte_circuit()
print(qc.draw())

Each shot now produces an 8-bit string like 10110011 — a random number between 0 and 255. With 4,096 shots, you get 4,096 random bytes in a single quantum circuit execution.

Why Hadamard creates uniform randomness

The Hadamard gate transforms |0⟩ into (|0⟩ + |1⟩) / √2. This is a perfectly balanced superposition. When you apply it to N qubits independently, you create a uniform superposition over all 2N possible bitstrings. Each bitstring has exactly the same probability of being measured.

For 8 qubits: 28 = 256 possible outcomes, each with probability 1/256. That is a perfectly uniform distribution — the gold standard for random number generation.

Part 3: Entangled QRNG — Our Approach

For the Quantum Genesis collection, we went a step further. Instead of independent qubits, we used entanglement to create correlated quantum randomness.

Why entanglement?

Independent Hadamard qubits produce random bits that are statistically independent. That is fine for basic RNG. But entanglement creates correlations between qubits that enrich the statistical structure of the output. The resulting distributions are more complex and, when hashed, produce higher-quality seeds for downstream applications.

Entanglement also makes the circuit more sensitive to the quantum processor's noise characteristics, which was exactly what we wanted — each processor's unique noise profile becomes part of the art.

The circuit: Hadamard + CNOT chain

from qiskit.circuit import QuantumCircuit

def build_nft_seed_circuit(num_qubits=12):
    """Build a maximally entangled circuit for quantum seed generation.

    H layer creates superposition on all qubits.
    CNOT chain creates maximum entanglement between neighbors.
    Result: a complex entangled state that produces rich measurement statistics.
    """
    qc = QuantumCircuit(num_qubits, num_qubits)

    # Step 1: Hadamard layer — put every qubit in superposition
    for i in range(num_qubits):
        qc.h(i)

    # Step 2: CNOT chain — entangle neighboring qubits
    for i in range(num_qubits - 1):
        qc.cx(i, i + 1)

    # Step 3: Measure all qubits
    for i in range(num_qubits):
        qc.measure(i, i)

    return qc

circuit = build_nft_seed_circuit(12)
print(circuit.draw())

Output:

      ┌───┐
 q_0: ┤ H ├──■───────────────────────────────────────────M──
      ├───┤┌─┴─┐                                         ║
 q_1: ┤ H ├┤ X ├──■──────────────────────────────────M───║──
      ├───┤└───┘┌─┴─┐                                ║   ║
 q_2: ┤ H ├────┤ X ├──■──────────────────────M───────║───║──
      ├───┤    └───┘┌─┴─┐                    ║       ║   ║
 ...  (continues for all 12 qubits)

The CNOT gates create a chain of entanglement. After the circuit runs, measuring qubit 0 instantly influences the probability distribution of qubit 1, which influences qubit 2, and so on down the chain. The qubits are no longer independent — they are a single entangled quantum system.

This is the exact circuit we used to generate the quantum seeds for all 100 NFTs in the Quantum Genesis collection. NFTs #19–100 were generated on ibm_fez (156 qubits) and ibm_torino (133 qubits).

Part 4: Running on Real Quantum Hardware

Now the exciting part: running your circuit on an actual quantum computer. IBM provides free access through the qiskit-ibm-runtime package.

Step 1: Connect to IBM Quantum

from qiskit_ibm_runtime import QiskitRuntimeService

# First time only: save your credentials
QiskitRuntimeService.save_account(
    channel="ibm_quantum_platform",
    token="YOUR_IBM_QUANTUM_TOKEN",
    overwrite=True
)

# Connect to the service
service = QiskitRuntimeService(channel="ibm_quantum_platform")

Step 2: Select a backend

# Find the least busy real quantum processor with at least 12 qubits
backend = service.least_busy(
    simulator=False,
    min_num_qubits=12
)
print(f"Selected backend: {backend.name}")
# e.g., "ibm_fez" (156 qubits) or "ibm_torino" (133 qubits)

Step 3: Transpile the circuit

Quantum processors have specific hardware constraints — not all qubits are connected to each other, and only certain gates are natively supported. Transpilation adapts your abstract circuit to the physical hardware:

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Create a pass manager optimized for this specific backend
pm = generate_preset_pass_manager(
    backend=backend,
    optimization_level=1  # 0=none, 1=light, 2=medium, 3=heavy
)

# Transpile: map logical qubits to physical qubits
isa_circuit = pm.run(circuit)
print(f"Circuit depth after transpilation: {isa_circuit.depth()}")

Step 4: Run with SamplerV2

from qiskit_ibm_runtime import SamplerV2 as Sampler

# Create sampler bound to our backend
sampler = Sampler(mode=backend)

# Run the circuit with 4096 shots
job = sampler.run([isa_circuit], shots=4096)
print(f"Job ID: {job.job_id()}")
print("Waiting for results from quantum hardware...")

# Get results (this blocks until the job completes)
result = job.result()
print("Done!")

Depending on the queue, this may take anywhere from a few seconds to a few minutes. Your circuit is being executed on real superconducting qubits cooled to 15 millikelvin — colder than outer space.

Step 5: Extract the measurement data

# SamplerV2 stores results in the classical register 'c'
counts = result[0].data.c.get_counts()

print(f"Number of unique bitstrings: {len(counts)}")
print(f"Total shots: {sum(counts.values())}")

# Show top 5 most frequent bitstrings
sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)
for bitstring, count in sorted_counts[:5]:
    print(f"  {bitstring}: {count} ({count/4096*100:.1f}%)")

Important note about SamplerV2: The result data is accessed via result[0].data.c where c is the classical register name. This is different from older Qiskit versions that used .data.meas.

Quantum Genesis #50 generated from IBM Quantum ibm_fez measurement data using Qiskit QRNG

Quantum Genesis #50 — This piece's entire color palette, composition, and texture were generated from quantum measurement data using the exact code in this tutorial.

Part 5: Converting Measurements to Usable Random Data

Raw quantum measurements give you a dictionary of bitstrings and counts. To use this as a general-purpose random seed, we hash the entire measurement distribution:

Step 1: Create a deterministic representation

import hashlib
import json

def measurements_to_seed(counts: dict) -> str:
    """Convert quantum measurement counts to a hex seed.

    Sorts the bitstrings for determinism, then hashes with SHA-256.
    The resulting hex string can seed any RNG or be used directly.
    """
    # Sort for deterministic ordering
    sorted_data = json.dumps(counts, sort_keys=True)

    # SHA-256 hash of the full measurement data
    hex_seed = hashlib.sha256(sorted_data.encode()).hexdigest()

    return hex_seed

seed = measurements_to_seed(counts)
print(f"Quantum seed: {seed}")
# e.g., "a3f8b2c1d4e5f6..."  (64 hex chars = 256 bits)

Step 2: Use the seed in your application

import random

# Option A: Seed Python's random module
random.seed(int(seed, 16))
print(f"Random float: {random.random()}")
print(f"Random int 1-100: {random.randint(1, 100)}")

# Option B: Use as raw bytes for cryptographic applications
raw_bytes = bytes.fromhex(seed)
print(f"Random bytes: {raw_bytes.hex()}")

# Option C: Feed into a custom RNG (like our QuantumRNG class)
# This is what we did for the NFT art generation

Our QuantumRNG class

For the Quantum Genesis collection, we used a custom xorshift128+-style RNG seeded from a SHA-512 hash of the quantum data. This lets us generate millions of random values from a single quantum seed while preserving the quantum origin:

import hashlib

class QuantumRNG:
    """PRNG seeded from quantum measurement data via SHA-512.
    Uses xorshift128+ for fast, high-quality generation."""

    MASK = 0xFFFFFFFFFFFFFFFF  # 64-bit mask

    def __init__(self, hex_seed: str):
        digest = hashlib.sha512(hex_seed.encode()).hexdigest()
        self.state = [
            int(digest[i:i+16], 16)
            for i in range(0, 64, 16)
        ]

    def next_float(self) -> float:
        """Return a random float in [0, 1)."""
        s1 = self.state[0]
        s0 = self.state[1]
        self.state[0] = s0
        s1 ^= (s1 << 23) & self.MASK
        self.state[1] = s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26)
        result = (self.state[1] + s0) & self.MASK
        return result / (1 << 64)

    def next_int(self, min_val: int, max_val: int) -> int:
        """Return a random integer in [min_val, max_val]."""
        return min_val + int(self.next_float() * (max_val - min_val + 1))

# Usage
rng = QuantumRNG(seed)
print(f"Quantum random float: {rng.next_float()}")
print(f"Quantum random color: #{rng.next_int(0, 0xFFFFFF):06x}")

Part 6: Verifying Randomness Quality

How do you know your quantum random numbers are actually random? You test them. Here are two standard statistical tests:

Chi-Squared Test

The chi-squared test checks whether your measurement distribution deviates significantly from uniform:

from scipy import stats
import numpy as np

def chi_squared_test(counts: dict, num_qubits: int, shots: int):
    """Test whether measurement counts follow a uniform distribution."""
    num_outcomes = 2 ** num_qubits
    expected = shots / num_outcomes  # Expected count per bitstring

    observed = np.zeros(num_outcomes)
    for bitstring, count in counts.items():
        index = int(bitstring, 2)
        observed[index] = count

    chi2, p_value = stats.chisquare(observed, f_exp=[expected] * num_outcomes)

    print(f"Chi-squared statistic: {chi2:.2f}")
    print(f"P-value: {p_value:.6f}")
    print(f"Result: {'PASS (uniform)' if p_value > 0.01 else 'FAIL (non-uniform)'}")

    return p_value

chi_squared_test(counts, num_qubits=12, shots=4096)

Shannon Entropy

Shannon entropy measures the information content of a distribution. Maximum entropy means maximum randomness:

import math

def shannon_entropy(counts: dict, shots: int) -> float:
    """Calculate Shannon entropy of measurement distribution.

    Maximum entropy for N outcomes = log2(N).
    For 12 qubits: max entropy = 12.0 bits.
    """
    entropy = 0.0
    for count in counts.values():
        if count > 0:
            p = count / shots
            entropy -= p * math.log2(p)

    max_entropy = math.log2(2 ** 12)  # = 12.0 for 12 qubits
    efficiency = entropy / max_entropy * 100

    print(f"Shannon entropy: {entropy:.4f} bits")
    print(f"Maximum possible: {max_entropy:.4f} bits")
    print(f"Efficiency: {efficiency:.1f}%")

    return entropy

shannon_entropy(counts, shots=4096)

For our Quantum Genesis collection, all 100 seeds passed both tests with high entropy scores. The quantum hardware consistently produced near-maximum entropy distributions.

All 100 Quantum Genesis NFTs were generated from quantum seeds that passed statistical randomness verification. The entropy scores are stored as on-chain attributes for each piece.

Complete Code: Full Working QRNG

Here is everything combined into a single, ready-to-run script:

"""
Quantum Random Number Generator using Qiskit + IBM Quantum
Generates true random numbers from real quantum hardware.

Usage:
    pip install qiskit qiskit-ibm-runtime
    python qrng.py
"""

import hashlib
import json
import math
from qiskit.circuit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

def build_qrng_circuit(num_qubits: int = 12) -> QuantumCircuit:
    """Build an entangled QRNG circuit (H + CNOT chain)."""
    qc = QuantumCircuit(num_qubits, num_qubits)
    for i in range(num_qubits):
        qc.h(i)
    for i in range(num_qubits - 1):
        qc.cx(i, i + 1)
    for i in range(num_qubits):
        qc.measure(i, i)
    return qc

def run_on_hardware(circuit: QuantumCircuit, shots: int = 4096) -> dict:
    """Run circuit on real IBM quantum hardware."""
    service = QiskitRuntimeService(channel="ibm_quantum_platform")
    backend = service.least_busy(simulator=False, min_num_qubits=12)
    print(f"Running on {backend.name}...")

    pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
    isa_circuit = pm.run(circuit)

    sampler = Sampler(mode=backend)
    job = sampler.run([isa_circuit], shots=shots)
    result = job.result()

    return result[0].data.c.get_counts()

def counts_to_seed(counts: dict) -> str:
    """Convert measurement counts to a SHA-256 hex seed."""
    sorted_data = json.dumps(counts, sort_keys=True)
    return hashlib.sha256(sorted_data.encode()).hexdigest()

def verify_entropy(counts: dict, shots: int, num_qubits: int):
    """Print Shannon entropy of the measurement distribution."""
    entropy = 0.0
    for count in counts.values():
        if count > 0:
            p = count / shots
            entropy -= p * math.log2(p)
    max_ent = math.log2(2 ** num_qubits)
    print(f"Entropy: {entropy:.4f} / {max_ent:.4f} bits ({entropy/max_ent*100:.1f}%)")

if __name__ == "__main__":
    NUM_QUBITS = 12
    SHOTS = 4096

    print("Building quantum circuit...")
    circuit = build_qrng_circuit(NUM_QUBITS)

    print("Submitting to quantum hardware...")
    counts = run_on_hardware(circuit, SHOTS)

    seed = counts_to_seed(counts)
    print(f"\nQuantum seed: {seed}")

    verify_entropy(counts, SHOTS, NUM_QUBITS)
    print(f"\nUnique bitstrings measured: {len(counts)}")
    print("Done! Use this seed for any application requiring true randomness.")

That is under 60 lines of code for a complete, working quantum random number generator that runs on real hardware. Copy it, run it, and you will have true quantum random numbers in minutes.

What You Can Build With This

A QRNG is a foundation. Here are some things you can build on top of it:

  • Generative art — like our Quantum Genesis collection
  • Cryptographic key generation — quantum-grade entropy for encryption keys
  • Fair lotteries and gaming — provably unbiased random selection
  • Monte Carlo simulations — scientific computing with true randomness
  • NFT trait generation — on-chain verifiable quantum randomness for rarity

The quantum computing era is here, and IBM gives you free access to it. There has never been a better time to start experimenting.

Quantum Genesis #99 — abstract art generated by quantum random number generator built with Qiskit tutorial code

Quantum Genesis #99 — generated using the exact QRNG code from this tutorial, running on IBM's ibm_fez quantum processor.

@media (prefers-color-scheme: dark) { background:#1a1a2e; border-color:#2d2d4a; box-shadow:0 1px 3px rgba(0,0,0,0.3); }
Marcelo Santos
Marcelo Santos
Engenheiro Quântico • Artista Generativo • Founder Quantum Art Lab

Desenvolvo sistemas que usam hardware quântico real (IBM Quantum, Origin Quantum) para gerar arte e NFTs com proveniência verificável on-chain. Escrevo sobre computação quântica aplicada, criptografia, Web3 e arte generativa — tudo com código que roda em processadores quânticos de verdade.

Comentários

Postagens mais visitadas deste blog

How to List NFTs on OpenSea: The Complete 2026 Guide

Polygon vs Ethereum for NFTs: Why We Chose Polygon (and Saved $490)

Quantum Error Correction Explained: Why Your Qubits Need Backup