Connecting real IBM hardware to your Python art pipeline
This is the exact method that seeded NFTs #19–100 in the Quantum Genesis collection — 82 pieces, each one grounded in a measurement from a real IBM Quantum processor. When I got it working I was mildly annoyed at how short the script ended up being, because the journey to it was not. The whole thing fits in under 50 lines of Python.
If you follow along, you'll end with a working quantum random seed generator: a job submitted to real hardware, a 12-qubit circuit, and a deterministic hex seed on the other side, ready to drive any generative art system.
Should I set expectations? The code is the easy part. What actually costs you time is the moving parts around it — the channel name that changed, the SamplerV2 result format that isn't meas anymore, and a queue that doesn't care about your evening plans. I've flagged each one where it bites.
What you need before starting
- Python 3.9+
- A free IBM Quantum account at quantum.ibm.com
- Basic Python fluency (functions, imports, dicts)
- About 15 minutes of setup, then 1–5 minutes of queue wait per job
What you'll build: a script that connects to a real IBM Quantum processor, runs a 12-qubit circuit, collects the measurement counts, and hashes them into a deterministic hex seed for any art pipeline.
Step 1 — install Qiskit
Two packages: qiskit (the SDK) and qiskit-ibm-runtime (the door to IBM's cloud processors).
pip install qiskit qiskit-ibm-runtime
That gets you Qiskit 1.x with the Runtime primitives API. If you already have an older Qiskit, pip install --upgrade qiskit qiskit-ibm-runtime will sort it out.
Step 2 — get your token, keep it out of code
Sign up at quantum.ibm.com, log in, and copy your API token from the account page. The free tier gives you access to real processors with fair-share queuing, no credit card required. The cloud lineup included ibm_fez (156 qubits), ibm_torino (133 qubits), ibm_marrakesh (156 qubits), and ibm_kingston (156 qubits) when we were running.
Store it as an environment variable, not a string in your repo:
# Linux/macOS
export IBM_QUANTUM_TOKEN="your_token_here"
# Windows PowerShell
$env:IBM_QUANTUM_TOKEN = "your_token_here"
Step 3 — initialize the service
import os
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
token = os.environ["IBM_QUANTUM_TOKEN"]
service = QiskitRuntimeService(
channel="ibm_quantum_platform",
token=token
)
The channel argument must be "ibm_quantum_platform". That's the current name — older tutorials show "ibm_quantum", which no longer authenticates. This exact rename burned me for an afternoon.
Step 4 — don't hardcode a backend
Instead of pinning a specific processor, query everything real and pick the least-busy one:
backends = service.backends(
simulator=False,
operational=True,
min_num_qubits=8
)
backends.sort(key=lambda b: b.status().pending_jobs)
backend = backends[0]
print(f"Selected backend: {backend.name} ({backend.num_qubits} qubits, "
f"{backend.status().pending_jobs} pending jobs)")
Resilient by design: if ibm_fez drops into maintenance, the script just picks the next option. For the collection, most jobs landed on ibm_fez (156 qubits) and ibm_torino (133 qubits).
Step 5 — build the circuit
The core of seed generation: a 12-qubit circuit built for maximum entropy.
from qiskit import QuantumCircuit
def build_nft_seed_circuit(n_qubits=12):
"""H + alternating CNOT max-entropy seed-generation circuit."""
qc = QuantumCircuit(n_qubits, n_qubits)
# Hadamard layer: put every qubit into superposition
for i in range(n_qubits):
qc.h(i)
# Even CNOT layer: entangle pairs (0,1), (2,3), (4,5)...
for i in range(0, n_qubits - 1, 2):
qc.cx(i, i + 1)
# Odd CNOT layer: entangle pairs (1,2), (3,4), (5,6)...
for i in range(1, n_qubits - 1, 2):
qc.cx(i, i + 1)
# Measure all qubits
qc.measure(range(n_qubits), range(n_qubits))
return qc
circuit = build_nft_seed_circuit()
The Hadamards put every qubit into superposition. The alternating CNOT layers entangle them — correlations between qubits with no classical analog. When measured, those correlations become a probability distribution no pseudorandom generator can fake.

Step 6 — transpile, then run
Real processors have connectivity constraints: not every qubit can talk to every other. Transpilation rewrites your abstract circuit to match the hardware's topology.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Transpile for the selected backend
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = pm.run(circuit)
# Run on real hardware using SamplerV2
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=4096)
print(f"Job ID: {job.job_id()} — waiting for results...")
result = job.result()
optimization_level=1 balances compile speed against circuit optimization. Levels 2 and 3 shave gates but slow compilation; for seed generation, level 1 is plenty. Then you wait. Expect 30 seconds to 5 minutes on the free tier — job.result() blocks until the job finishes.
Step 7 — extract counts, hash to a seed
SamplerV2 returns results in a specific shape, and the classical register is named c, not meas. Getting that wrong is the most common source of confusion:
import hashlib
# Extract measurement counts from SamplerV2 result
pub_result = result[0]
counts = pub_result.data.c.get_counts()
# Convert counts to deterministic hex seed
sorted_counts = sorted(counts.items())
raw = "|".join(f"{k}:{v}" for k, v in sorted_counts)
seed = hashlib.sha256(raw.encode()).hexdigest()
print(f"Quantum seed: {seed}")
# Example: "a3f7c1d8e5b2094f6..." (64-char hex string)
Why SHA-256? Raw counts are noisy and variable-length. Hashing normalizes them into a fixed 256-bit value with a uniform distribution — ideal for seeding any PRNG or art generator. Same counts, same seed, always; flip one bit in the measurement and the hash is unrecognizable.
Step 8 — feed the seed into your art generator
A 64-character hex seed can drive any generative art pipeline. Minimal example using Python's random:
import random
# Use the quantum seed for deterministic art generation
rng = random.Random(seed)
# Generate art parameters from quantum randomness
palette_index = rng.randint(0, 15)
symmetry = rng.choice(["radial", "bilateral", "asymmetric"])
complexity = rng.uniform(0.3, 1.0)
hue_base = rng.randint(0, 360)
print(f"Palette: {palette_index}, Symmetry: {symmetry}")
print(f"Complexity: {complexity:.2f}, Hue: {hue_base}")
# Pass these parameters to your SVG/Canvas/PIL renderer...
For Quantum Genesis, that seed drove every visual attribute: qubit configuration (GHZ-3, Steane-7, and friends), color palette, geometric structure, and fine detail. Each NFT's metadata records the exact quantum backend and shot count.

The whole thing in one file
"""Quantum random seed generator for generative art.
Connects to IBM Quantum, runs a 12-qubit circuit, returns a hex seed."""
import os
import hashlib
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
def build_nft_seed_circuit(n_qubits=12):
qc = QuantumCircuit(n_qubits, n_qubits)
for i in range(n_qubits):
qc.h(i)
for i in range(0, n_qubits - 1, 2):
qc.cx(i, i + 1)
for i in range(1, n_qubits - 1, 2):
qc.cx(i, i + 1)
qc.measure(range(n_qubits), range(n_qubits))
return qc
def generate_quantum_seed():
# Connect to IBM Quantum
service = QiskitRuntimeService(
channel="ibm_quantum_platform",
token=os.environ["IBM_QUANTUM_TOKEN"]
)
# Pick the least-busy real backend
backends = service.backends(simulator=False, operational=True, min_num_qubits=8)
backends.sort(key=lambda b: b.status().pending_jobs)
backend = backends[0]
print(f"Backend: {backend.name} ({backend.num_qubits}q)")
# Build, transpile, and run
circuit = build_nft_seed_circuit()
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=4096)
result = job.result()
# Extract counts and hash into seed
counts = result[0].data.c.get_counts()
sorted_counts = sorted(counts.items())
raw = "|".join(f"{k}:{v}" for k, v in sorted_counts)
seed = hashlib.sha256(raw.encode()).hexdigest()
print(f"Quantum seed: {seed}")
return seed
if __name__ == "__main__":
generate_quantum_seed()
Save it as quantum_seed.py, set IBM_QUANTUM_TOKEN, and run it. About 1–5 minutes later you have a 64-character hex seed derived from a real quantum measurement.

The mistakes I made so you don't
Wrong channel name. "ibm_quantum_platform", never "ibm_quantum". The old name raises an authentication error on current Qiskit Runtime.
SamplerV2 result format. Access counts through result[0].data.c. If you gave the circuit's classical register a custom name, use that name; .data.meas is long gone.
Transpilation is mandatory. You cannot submit an abstract circuit straight to hardware. generate_preset_pass_manager handles gate decomposition, qubit routing, and optimization for the specific processor. Skipping it errors out immediately.
Queue times vary. Free-tier jobs share the processor fairly. At peak hours you can wait 5–10 minutes; sorting backends by pending jobs keeps that short. For batch generation, fire multiple jobs and collect results asynchronously instead of one blocking call at a time.
Shots change the seed's statistics. More shots, more measurement data, richer probability info. We used 4,096 (IBM's default). Dipping below ~1,000 starts producing distributions that are statistically thin; for art, 4,096 is a solid balance of quality and wall-clock time.
From here the same pipeline extends to SVG rendering, PNG export, IPFS upload, and minting — the full 100-piece collection is on-chain with metadata tracing each piece back to its processor. I'll cover the SVG-to-IPFS-to-contract leg in the next post; if your seed generation now works and you have a pointer, drop it in the comments.
Comments
Post a Comment