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.

Quantum Genesis NFT #19 — first NFT generated from IBM Quantum ibm_fez processor

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.

Quantum Genesis NFT #42 — generative art created from IBM Quantum measurement data

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.

Quantum Genesis NFT #100 — final piece in the collection, from IBM Quantum

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

Popular posts from this blog

Getting Your Collection Visible on OpenSea, Step by Step

Polygon versus Ethereum for an NFT contract, from the gas bills up

Quantum Error Correction, or Why Your Qubits Forget What They Were Doing