The quantum muse: harnessing randomness for artistic creation

Juna Salviati
4 min readApr 28, 2024

Introducing quantum computing in generative art is (theoretically) easier than you think

Back at the end of December 2023 I ran into “GENUARY24”, which is like an “Advent of Code” but for generative art.

That challenge intrigued me, since I thought it could be a nice way to experiment some new solutions and to go deep into art themes, which is the kind of heterogeneity I like.

I quickly skimmed along the challenge prompts and finally decided to spend some time experimenting with algorithms to generate prompt n.5: “In the style of Vera Molnár (1924–2023)”

Prompt #5: “In the style of Vera Molnàr”

Vera Molnàr used computers to generate her artistic works: she was a pioneer in generative art and thought that randomness could replace artist’s intuition.

All her works are “computed” starting from very basic forms (lines, squares, colors) and all are influenced by some kind of “noise”.

Classical computers can produce pseudo-random numbers; quantum computers can achieve true randomness instead, so I liked the idea of using quantum computing to generate “Vera Molnàr-like” pictures.

In “(Des)Ordres” artworks (1974), the artist creates a series of concentric squares, in random sizes and colors.

I first wrote a classical algorithm, just to have a clear idea about where randomness kicks in and the size of the random number to be cast:

import random
from PIL import Image, ImageDraw


colors = ["cyan","green","red","violet","blue","orange","pink"]
size = 150
n_tiles = 8


img = Image.new("RGB", (size*n_tiles,size*n_tiles), color="#EDEBDF")
img1 = ImageDraw.Draw(img)


def draw_tile(img, size, colors, position):
t_x, t_y = position
square_size = random.randint(10,size)
c = random.randint(0,len(colors))
for i in range(0,5):
square_size = random.randint(10,(size//2)-10)
shape = [(t_x+square_size, t_y+square_size), (t_x+size - square_size, t_y+size - square_size)]
img.rectangle(shape, outline=colors[(c+i) % len(colors)],width=2)


for i in range(n_tiles):
for j in range(n_tiles):
draw_tile(img=img1,size=size,colors=colors,position=(size*j,size*i))
img.save('out.png')

A circuit to cast random numbers

It is possible to obtain random numbers by using Hadamard gates to induce a superposition on qubits.

Since the tile size is 150 here, I first wrote a circuit to cast a number between 0 and 2^n, where n is the smallest exponent required to exceed 150.

n is also the number of qubits we need to measure in order to obtain that random number and the number of classical bits to collapse the measures on.

Then I tried to generalize the circuit to cast numbers between 0 and a generic number m, calculating the exponent as part of the process.

In the following figure, for clarity’s sake, I show how to develop a circuit to generate numbers between 0 and 2³.

Here, Hadamard equally distributes probabilities among all the numbers:

Probabilities after applying Hadamard gates to 3 qubits
Applying Hadamard gates to 3 qubits

Qiskit implementation

I developed the circuit with Qiskit as in the following snippet of code:

def build_randint_circuit(n_max):
# build a circuit with n qubits, in superposition, to be measured
# this will generate a random number between [0,n_max]
n_bits = math.ceil(math.log(n_max, 2))
qreg_q = QuantumRegister(n_bits, 'q')
creg_c = ClassicalRegister(n_bits, 'c')
circuit = QuantumCircuit(qreg_q, creg_c)
for i in range(0, n_bits):
circuit.h(qreg_q[i])
circuit.measure(qreg_q[i], creg_c[i])
return circuit

I just calculated the number of bits needed to encode the maximum number to be cast, initialized quantum and classical registers and used a for loop to apply a Hadamard gate to every single qubit in the circuit and measure it.

Then I just initialized and ran a simulation to obtain random numbers:

def qrandint_generator(n_min, n_max):
simulator = AerSimulator()
circuit = build_randint_circuit(n_max=n_max)
# Run and peek into system to get a random number
res = +math.inf
while res < n_min or res > n_max:
result = simulator.run(circuit, memory=True, shots=1).result()
data = result.get_memory()
res = int(data[0],2)
return res

I chose to manually discard all the numbers not in the range [n_min, n_max], but it is also possible to develop a circuit to discard unnecessary numbers directly on the circuit.

I think a number of considerations should be made here:

  • I’m pretty sure It is not too good to “run a job” every time a random number is needed, so it’s probably better to run the simulation to obtain a list of random numbers to be used later, in order to reduce the number of the job to be run once the algorithm is going to be launched on a real quantum device.
  • It is known that quantum information just decays with running time/number of qubits and that is to be taken in account when dealing with a real quantum device. Simulation is ok to “theory-proof” an algorithm but then a number of factors kicks in on real devices (e.g: noise)

Now that we have the “quantum random number generator”, we can substitute all the random parts in the original algorithm:

def draw_tile(img, size, colors, position):
t_x, t_y = position
square_size = random.randint(10,size)
# color_idx = random.randint(0,len(colors))
color_idx = qrandint_generator(0,len(colors)-1)
for i in range(0,5):
square_size = qrandint_generator(10,(size//2)-10)
# square_size = random.randint(10,(size//2)-10)
shape = [(t_x+square_size, t_y+square_size), (t_x+size - square_size, t_y+size - square_size)]
img.rectangle(shape, outline=colors[(color_idx+i) % len(colors)],width=2)

for i in range(n_tiles):
for j in range(n_tiles):
draw_tile(img=img1,size=size,colors=colors,position=(size*j,size*i))
img.save('out.png')

To obtain the following output image:

A “Vera Molnàr”-like “quantum powered” generated artwork
A “Vera Molnàr”-like “quantum powered” generated artwork

--

--

Juna Salviati

Full-time Human-computer interpreter. Opinions are my own.