Our goal in this new blog article is to use quantum computers to produce a random binary string, where each bit in the string is randomly chosen. In cryptography, the generation of random strings, and random numbers in general, is an important application, quantum Computers can be applied to random string generation, using the computer as a random number generator, taking advantage of the "statistically observed" "quantum randomness".
So far we have worked our examples only using the circuit composer. In this article we work with Qiskit functionalities, our goal is to use the Python programming language and the package Qiskit to build a quantum random binary string generator, taking advantage of quantum parallelism and "quantum randomness", thus, to get a string of size $N$ we use $N$ quantum registers.
The first step is to import the main functionalities. which is done next:
%matplotlib inline
# Importing standard Qiskit libraries and configuring account
from qiskit import QuantumCircuit, execute, Aer, IBMQ
from qiskit.visualization import *
import numpy as np
# Loading your IBM Q account(s)
provider = IBMQ.load_account()
Having imported what we need, we are now ready to define the two main functions in the Python programming language.
The first function is the "setup_circuit" function that sets the quantum circuit so that each quantum register works as a basic coin. Which means that if our intended random binary string is of size $N$ we need the same number of quantum registers and the same number of classical registers.
Each quantum register will undergo an independent Haddamard transform and then it is measured, which makes the circuit generate each bit with a 1/2 probability (a point to which we will return further on).
The first function takes as inputs the string size and the backend on which to run the program and returns:
a) The device on which the circuit will be run (this is needed for the cloud access to IBM's quantum devices);
b) The quantum circuit that is to be run.
The second function, which we called "get_string" takes as inputs the device, the circuit and the number of shots. Now, if the number of shots is set to 1, the "get_string" function uses the cloud access to run the quantum circuit with one single shot (one single experiment), printing the binary string. If the number of shots is set to more than 1, the function prints the counts results and returns the counts for further processing.
In the Python code, we wrote the comments as pedagogical notes to provide an added explanation into the main structure of the code.
# Setup the circuit function
def setup_circuit(size_string, # size of random string
backend='qasm_simulator' # backend to be used, default is set to 'qasm_simulator'
):
# Get backend
if backend == 'qasm_simulator':
device = Aer.get_backend(backend) # use Aer if the backend is the 'qasm_simulator' (default)
else:
device = provider.get_backend(backend) # use the provider otherwise
# Setup the circuit (in the circuit we need the same number of quantum and classical registers)
circuit = QuantumCircuit(size_string,size_string)
# We use a for loop to setup the quantum gates
# so that for each quantum register apply the Haddamard transform, this is the unitary
# evolution stage
for i in range(0,size_string):
circuit.h(i)
# We use another for loop to setup the measurement process
for i in range(0,size_string):
circuit.measure(i,i)
# Return the device for implementing this circuit and the circuit with the above structure
return device, circuit
# Get the string function
def get_string(device, circuit, num_shots):
# Execute the circuit on the device for the number of shots defined
job = execute(circuit, device, shots=num_shots)
# Get the results
result = job.result()
# Get the counts
counts = result.get_counts(circuit)
# Either print statistical results for the problem or return a single random string
if num_shots > 1:
# If the number of shots is greater than 1 print the counts and return the counts
# for further processing, for instance, plotting or calculating other statistics
print(counts)
return counts
else:
# If the number of shots is equal to 1, then, the print the single randomly generated string
for key in counts.keys():
if counts[key] == 1:
print(key)
break
Having defined the functions, we are now ready to implement our Qiskit Python code. The first example that we provide is the generation of a random string with 100 digits. We cannot run this on the actual IBM physical devices, since it has more quantum registers than the available devices, which means that, in this first example, we will work with the simulator, which is used by default in our defined Python functions.
# Setup the string
device, circuit = setup_circuit(100)
# Run the string
get_string(device,circuit,num_shots=1)
The following experiment is run on the Burlington device, which is a 5 qubit actual quantum computer. In this experiment we use the five registers for a random generation of a 5 bit string.
device, circuit = setup_circuit(5,'ibmq_burlington')
get_string(device,circuit,num_shots=1)
To get a string larger than 5, we need a quantum computer with a larger number of qubits, the next experiment works with the Melbourne device and extracts a random string of 5 qubits.
device, circuit = setup_circuit(15,'ibmq_16_melbourne')
get_string(device,circuit,num_shots=1)
Let us now generate multiple strings of size 6, running the program on a quantum computer with multiple shots to see the statistical results that we may get from running the quantum program multiple times. We are still using the Melbourne device.
device, circuit = setup_circuit(6,'ibmq_16_melbourne')
counts = get_string(device,circuit,num_shots=1000)
Now, with the Burlington device and three qubit registers, the following code calculates the Histogram, using the get_string function to print and get the counts that are then used to plot the histogram.
device, circuit = setup_circuit(3,'ibmq_burlington')
counts = get_string(device,circuit,num_shots=8192)
plot_histogram(counts)
While there are some differences in the above histogram's bars, the results are close to $(1/2)^3=0.125$, the difference is due to the fact that a sample of experiments is being plotted, as the number of experiments rises, the relative frequencies will tend to the true probability $(1/2)^N$. We can see this convergence occurring more for a lower string size, as the following experiment shows.
device, circuit = setup_circuit(2,'ibmq_burlington')
counts = get_string(device,circuit,num_shots=8192)
plot_histogram(counts)
Let us now write a new function, in this case, to calculate the experimental entropy for repeated runs of the circuit.
The first point that we need to stress is that, for the ideal system, where the unitary evolution holds perfectly, which corresponds to the case of the ideal computer without errors, the entropy is null for the pure density that results from the unitary evolution stage. Given the circuit's structure, where the evolution for each quantum register is independent, we can address the process in terms of the local evolution for each register, which, in this circuit, is the same for each of them.
First, under the unitary evolution, each register's quantum dynamics leads to an equally weighted superposition of |0> and |1>, which is described by the symmetric ket $ |+> = (|0> + |1>)/sqrt(2) $, which, expressed as a density operator, leads to the local transition for each quantum register:
$ |0><0| -> |+><+| = (|0><0| + |0><1| + |1><0| + |1><1|)/sqrt(2) $
Now, considering the "classical register" for each quantum register, which corresponds to our measurement apparatus and including, in our description, the "rest of the world" that we write as "World", the quantum measurement leads to the following density after measurement:
$ A = |+,C,World><+,C,World| $,
where the ket |+,C,World> is the entangled result:
$ |+,C,World> = (|0,C_0,World_0>+|1,C_1,World_1>)/sqrt(2) $.
Now, since we are part of the rest of the world and we only see the results from the classical register, we can address our knowledge in terms of the register plus rest of the world configuration, which leads to the reduced density:
$ R = 0.5*|C_0,World_0><C_0,World_0|+0.5*|C_1,World_1><C_1,World_1| $
Therefore, we have a classical mixture with one branch corresponding to a world where the classical register (measurement device) registers the value '0' which is returned and seen by us, and a world where the classical register (measurement device) registers the value '1' which is returned and seen by us. The resulting von Neumann entropy, measured in the base 2, for this last density, is equal to 1 bit, which is a maximum entropy value.
Likewise, if we consider all of the registers then we get the tensor product of $N$ copies of the density R, leading to an entropy equal to $N$ bits, therefore, each register's (measurement) results are similar to an idealized balanced coin, and the $N$ registers behave similar to $N$ coin tosses, or, in this case, $N$ coins being tossed.
It's important to stress that, different interpretations of quantum mechanics will interpret the above results differently, however, we will not be discussing here these different interpretations, we are mostly interested, from a quantum cybersecurity applications' standpoint, in testing the randomness of the experimental process, namely, we want to test whether the distribution over the binary strings obtained from the measurement process is effectively close to the maximum entropy result, which indicates that the quantum computer does what we wanted, that is, to produce strings of binary digits that behave like the result of an "idealized" balanced coin toss.
The following Python allows us to obtain this evaluation.
def calculate_entropy(device,circuit,num_shots):
# Execute the circuit on the device for the number of shots defined
job = execute(circuit, device, shots=num_shots)
# Get the results
result = job.result()
# Returns counts
counts = result.get_counts(circuit)
# Calculate and print the entropy
H=0
for key in counts.keys():
p=counts[key]/num_shots
if p != 0:
x = -p*np.log(p)/np.log(2)
H += x
print("Entropy value", H)
Let us now run the function for a quantum single register, on the Burlington device.
device, circuit = setup_circuit(1,'ibmq_burlington')
calculate_entropy(device,circuit,num_shots=8192)
The value obtained in our run was 0.999966291137892, slightly lower but very close to 1 bit. Let us now run the experiment for 2 registers
device, circuit = setup_circuit(2,'ibmq_burlington')
calculate_entropy(device,circuit,num_shots=8192)
In this case, we got the number 1.9976699397719135, which is also lower but close to 2 bits. For a Final experiment let us run the circuit with 6 registers, on both the "qasm" simulator and the Melbourne device. In the case of the "qasm" we use 100,000 repeated trials, in the case of the Melbourne device we will use 8,192, which is the largest value that we can set.
device, circuit = setup_circuit(6)
calculate_entropy(device,circuit,num_shots=100000)
device, circuit = setup_circuit(6,'ibmq_16_melbourne')
calculate_entropy(device,circuit,num_shots=8192)
In both cases we got values lower but close to the maximum entropy.
Why is this?
Well, first we will never get a value larger than the maximum entropy, otherwise it would not be maximum, secondly, in any finite number of experiments, we can expect to get relative frequencies close to the theoretical values but not exactly equal, since the probability is the number to which the relative frequencies tend as the number of trials rise, which means that we can expect, on a finite number of experiments with random number generation (quantum or not), lower values than the maximum but, as long as the random number generator is working properly, close to the maximum.
When we use a pseudorandom number generator, we are using a method for random number generation that is actually deterministic, the same "pseudorandomness" holds for the device 'qasm_simulator', on the other hand, when we run the experiment on the actual quantum computers, we are using quantum randomness to generate the strings. Since the probability for each result is close to the balanced coin toss we get very close to our intended profile of random binary string generation.
There is, of course, one point that can be risen: is quantum randomness true randomness? That is a problematic question on three points:
1) What does "true randomness mean"? 2) What does "being random" mean? 3) What is "quantum randomness"?
From a practical standpoint, we can satisfy ourselves that the quantum computers produce a result that closely approximates our models/mathematical criteria for randomness, in terms of probability theory. In that sense, the pragmatic and, perhaps, safer answer would be that the quantum computer running our Python coded algorithm does behave like a random number generator that closely approximates our model of a balanced coin, which means that it works as far as our technological intention goes.
The Philosophical reflection on "true randomness" and "quantum randomness" leads us to the questions of truth, reality, randomness, order and the quantum dynamics. In this regard, we have no final answer, in the sense that quantum theory's formalism is, so far, open to different interpretations, all consistent with the same formalism but that differ in regards to their stance on "reality", "randomness" and the "quantum dynamics", so the question "is quantum randomness true randomness?", for now, ends in the question mark, while, at the same time, we do the calculations and run our algorithms on quantum computers that work in accordance with the formalism and behave for all practical/applicable purposes in accordance with the probabilistic conception of randomness.