Qiskit Beginner Free 35/61 in series 10 min read

Building a Quantum Random Number Generator

Build a true quantum random number generator using superposition and measurement in Qiskit, and understand why quantum randomness is fundamentally different from classical pseudo-randomness.

What you'll learn

  • random number generator
  • QRNG
  • Hadamard
  • Qiskit
  • measurement
  • randomness

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

Every time you ask a computer to generate a random number, it runs an algorithm. That algorithm starts from a seed value and applies a deterministic sequence of operations. Given the same seed, you always get the same sequence. This is called a pseudo-random number generator (PRNG), and it is what powers everything from shuffled playlists to encrypted web traffic.

Quantum computers work differently. A qubit in superposition has no predetermined measurement outcome. Not just unknown, but genuinely undetermined. When you measure it, the universe decides. That physical process is the basis for a true random number generator.

Classical vs Quantum Randomness

A classical PRNG like the Mersenne Twister is a function f(seed) -> sequence. The sequence looks random, passes statistical tests, and is perfectly adequate for most purposes. But it is reproducible. If an attacker knows the seed or can observe enough output to reconstruct the internal state, they can predict future values.

Quantum randomness is different in kind, not just degree. The Born rule states that the probability of measuring a qubit in state |0> or |1> is given by the squared magnitude of the corresponding amplitude. There is no hidden variable that predetermined the outcome. No seed. No internal state to reconstruct. The outcome simply did not exist before measurement.

This distinction matters most in cryptography. For generating encryption keys, where the entire security of the system rests on the key being unknowable, true randomness is worth the overhead.

The Simplest QRNG

The simplest quantum random bit requires exactly one gate: a Hadamard. Starting from |0>, the H gate produces (|0> + |1>) / sqrt(2), an equal superposition with 50% probability of measuring either outcome.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

# Single-qubit QRNG
qc = QuantumCircuit(1, 1)
qc.h(0)          # Put qubit into superposition
qc.measure(0, 0) # Collapse and record

simulator = AerSimulator()
job = simulator.run(qc, shots=10)
result = job.result()
counts = result.get_counts()

print(counts)
# Example: {'0': 6, '1': 4}
# Distribution approaches 50/50 with more shots

Each shot produces one independent random bit. The distribution over many shots should be statistically indistinguishable from a fair coin.

To extract a single random bit from the results:

# Get just the first measurement result
job = simulator.run(qc, shots=1)
counts = job.result().get_counts()
random_bit = int(list(counts.keys())[0])
print(f"Random bit: {random_bit}")

Multi-Bit QRNG: One Random Byte Per Shot

Measuring n qubits in superposition produces n independent random bits simultaneously. With 8 qubits, you get one random byte per circuit execution.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

def qrng_byte():
    n = 8
    qc = QuantumCircuit(n, n)
    qc.h(range(n))         # All qubits in superposition
    qc.measure(range(n), range(n))

    simulator = AerSimulator()
    job = simulator.run(qc, shots=1)
    result = job.result()
    bitstring = list(result.get_counts().keys())[0]

    # Qiskit orders bits with qubit 0 on the right
    random_byte = int(bitstring, 2)
    return random_byte

print(qrng_byte())   # A random integer between 0 and 255

Each call executes the circuit once and returns a different random byte. Run it multiple times to collect a stream of random bytes.

To generate a sequence of random bytes efficiently, use multiple shots:

def qrng_bytes(count):
    n = 8
    qc = QuantumCircuit(n, n)
    qc.h(range(n))
    qc.measure(range(n), range(n))

    simulator = AerSimulator()
    job = simulator.run(qc, shots=count)
    counts = job.result().get_counts()

    # Expand counts dict into a flat list
    random_bytes = []
    for bitstring, freq in counts.items():
        value = int(bitstring, 2)
        random_bytes.extend([value] * freq)

    return random_bytes[:count]

print(qrng_bytes(10))
# Example: [173, 42, 209, 7, 88, 251, 130, 64, 19, 200]

Random Integer in a Range

Generating a random integer in [a, b] requires enough bits to cover the range, then rejection sampling to avoid bias.

import math

def qrng_int(a, b, simulator):
    span = b - a + 1
    n_bits = math.ceil(math.log2(span))

    while True:
        qc = QuantumCircuit(n_bits, n_bits)
        qc.h(range(n_bits))
        qc.measure(range(n_bits), range(n_bits))

        job = simulator.run(qc, shots=1)
        bitstring = list(job.result().get_counts().keys())[0]
        value = int(bitstring, 2)

        # Reject values outside the range to avoid modulo bias
        if value < span:
            return a + value

simulator = AerSimulator()
# Random integer between 1 and 100
print(qrng_int(1, 100, simulator))

Rejection sampling ensures the output distribution is exactly uniform over [a, b], with no bias from truncation.

Using Real IBM Quantum Hardware

The Aer simulator is deterministic at the algorithmic level: it uses classical floating-point arithmetic to compute probabilities, then samples from them using a classical PRNG internally. Simulated QRNG is therefore pseudo-random under the hood.

For genuine quantum randomness, you need to run on real hardware through IBM Quantum:

from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Authenticate with your IBM Quantum account
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

n = 8
qc = QuantumCircuit(n, n)
qc.h(range(n))
qc.measure(range(n), range(n))

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

sampler = Sampler(backend)
job = sampler.run([isa_circuit], shots=100)
result = job.result()
print(result[0].data.c.get_counts())

On real hardware, the randomness originates from quantum measurement. Shot noise, thermal fluctuations, and decoherence all contribute additional randomness beyond what the circuit itself generates.

Entropy Estimation and Randomness Testing

Claiming your generator is random is not the same as demonstrating it. The NIST Statistical Test Suite (SP 800-22) is the standard battery of tests used to evaluate random number generators for cryptographic applications. It includes 15 tests covering frequency, runs, block frequency, spectral analysis, and serial correlation.

To test your QRNG output informally:

from collections import Counter
import math

def estimate_entropy(bitstring):
    n = len(bitstring)
    counts = Counter(bitstring)
    entropy = -sum((c / n) * math.log2(c / n) for c in counts.values())
    return entropy

# Generate a long bitstring and check entropy
bits = ''.join(str(b % 2) for b in qrng_bytes(1000))
print(f"Entropy per bit: {estimate_entropy(bits):.4f} (ideal: 1.0000)")

A perfect single-bit source has entropy of exactly 1.0 bit per bit. Values below 0.99 suggest bias worth investigating.

Commercial QRNG Devices

Several commercial products deliver certified quantum randomness over USB or PCIe:

ID Quantique Quantis: Uses photon detection on a beam splitter. Available as a PCIe card or USB device. Certified to AIS-31 (German BSI) and FIPS 140-2 standards. Widely used in financial services and government applications.

ID Quantique IDQ20MC1: An integrated chip QRNG designed for embedded systems and IoT devices. Operates at tens of megabits per second.

QuintessenceLabs qStream: High-throughput QRNG for data center use, delivering gigabit-per-second random output certified for cryptographic key generation.

These devices can serve as hardware entropy sources for operating system /dev/random, OpenSSL, and key management systems without requiring access to a full quantum computer.

One-Time Pad: Perfect Secrecy from Perfect Randomness

The one-time pad is the only encryption scheme with information-theoretic security: it is mathematically impossible to crack, even with unlimited computing power, provided the key is truly random, as long as the message, and never reused.

def one_time_pad_encrypt(message_bytes, key_bytes):
    assert len(key_bytes) >= len(message_bytes), "Key must be at least as long as message"
    return bytes(m ^ k for m, k in zip(message_bytes, key_bytes))

message = b"hello"
key = bytes(qrng_bytes(len(message)))   # Quantum-random key
ciphertext = one_time_pad_encrypt(message, key)
plaintext = one_time_pad_encrypt(ciphertext, key)  # XOR again to decrypt

print(f"Key: {key.hex()}")
print(f"Ciphertext: {ciphertext.hex()}")
print(f"Decrypted: {plaintext}")

The catch: the key must be distributed securely, which is as hard as distributing the message itself. In practice, quantum key distribution (QKD) solves the key exchange problem while QRNG solves the key generation problem. Together they form the basis for information-theoretically secure communication.

Summary

Quantum randomness is fundamentally different from classical pseudo-randomness: it arises from the Born rule rather than a deterministic algorithm. A Hadamard gate plus measurement is all you need to generate a true random bit in Qiskit. Scaling to multi-bit output is straightforward, and real hardware delivers genuine quantum randomness where simulations cannot. For cryptographic applications, pair QRNG with proper randomness testing before deploying.

Further Reading

Was this tutorial helpful?