Quantum computers have the ability to create true random numbers, but in practice, many examples are inefficient. This article explores the creation of efficient random numbers in quantum circuits for practical applications.
Recently, I responded to a question on Quora’s Quantum Programming¹ space about putting a random H-Gate on a qubit using Jupyter Notebook. This is easily doable with common methods, however these methods are terribly inefficient.
This prompted me to consider how this could be done with more efficiency.
How To Create Randomness With Qubits
To create randomness with qubits, it’s important to understand the properties of quantum states. When qubits are in a superposition, they have no state but can become any state on a Bloch sphere in Hilbert space. When measured, their spin can point in any direction on the sphere. Northward spins are interpreted as spins up (binary 0), while southward spins are interpreted as spins down (binary 1).
Northward and southern spins encapsulate any spins above or below the red line shown in the diagram above. There are an infinite number of directions a spin can be facing, this reduces all of them to one of 2 possibilities.
To momentarily digress for those unfamiliar, quantum weirdness, such as quantum entanglement, plays a role in quantum circuits. Entangled particles no longer exist individually but rather as possibilities of infinite states. Observing one particle affects the state of the other, regardless of the distance or time between them. This allows simultaneous calculations on both 0 and 1, resulting in exponential processing power when combined with other qubits.
However, it is the probability of each physical state that forms the basis of creating random numbers. If a random number between 0 and 1 is required, reading a single qubit is sufficient. For larger numbers, the process is more involved.
To exploit this phenomenon in a quantum circuit, one common approach is to create a circuit that reads a single qubit and repeat it multiple times to generate random numbers within the desired range.
While this does work, there are more efficient ways to do this depending on the desired range. For example, if a random number ranging from 1 to 8 is required, three qubits can be measured to obtain a binary number ranging from 0 to 7, which corresponds to integers 1 through 8.
Exploiting Quantum States
As mentioned, the quickest and easiest method of generating a random number with a quantum processor is to create a simple circuit to read a single qubit and then execute the circuit as many times as necessary to reach the upper limit of the desired random number. For example, to generate a random number between 1 and 6, the circuit would be executed six times, incrementing an integer variable if the return is |1| and leaving it unchanged if the return is |0|. By the end of the process, the accumulated bits represent the generated random number.
The below example registers both a quantum and classical bit, sets q[0] into a superposition denoted by Ψ, and applies an H-Gate to the qubit:
Which is the equivalent of 0, the code looks like this:
\\ OpenQASM 2.0
qreg q[1]; \\ Define one qubit
creg c[1]; \\ Define one classical bit
reset q; \\ Reset quantum states
h q; \\ Apply H-Gate to qubit
measure q -> c; \\ Measure qubit q into classcal bit c
The H-Gate forces a state represented as:
This can be visualized as a pseudo-circuit:
The conversion of the classical bit can be described by using a function where x is a given qubit q. It returns a binary 0 if x equals ket 0, represented by the following math, or 1 if x equals ket 1.
This process needs to be repeated as many times as the desired number’s upper limit. While this method is popular, it can be computationally expensive. There are more efficient ways to achieve the same result depending on the desired level of randomization.
For example, if you want to select a random qubit out of 8, you can measure 3 qubits to obtain a binary representation of a decimal number from 0 to 7, which corresponds to qubits 1 through 8.
The code for this would look like:
qreg q[3];
creg c[3];
reset q;
h q;
measure q -> c;
The quantum circuit diagram for this looks like the following:
Since the original user who asked this question specifically mentioned Jupyter Notebook, the following example explains post-processing in Python using the Qiskit library.
Notably, post-processing could be done in QASM, but it can become very complicated and goes beyond the scope of this article.
To create the circuit using Python code, constructing the circuit by calling the QASM counterparts with library functions in Python would require more effort. A quicker way is to directly load the assembly code, as shown below:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
reset q;
h q;
measure q -> c;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
The counts variable will return a dictionary object with keys that can be converted from binary to decimal. This can be done by creating a set with the reverse order of the binary value.
In other words, if binary value q is 110 we put push value q into {1,1,0} and reverse the set (x in q) such that b = ℝ⁻¹. Here, ℝ⁻¹ represents the inverse set x, which is assigned to set b, thus b = q⁻¹.
Now that set b is established, ⌊log₁₀b⌋+1 equals the cardinal of the set, or the number of bits, which is the floor of log₁₀b, plus 1. To calculate the decimal number, each bit is multiplied by 2 to the power of (⌊log₁₀b⌋+1) from left to right, and the sum of all results equals the decimal conversion of the binary number. The set is inverted for efficiency, as the first number of the set requires the highest exponent. By using the inverse, i can be efficiently used for both the set and the exponent, like this:
Or simplified, cardinal notation |b| or #(b) can describe this:
In Python this can be done by looping through the dictionary objects, which are the bits, with the following code:
for x in counts.keys(): 00
n = int(x,2)
In the original question, an H-Gate applied to a random qubit was desired. In this case, the random n value representing qubits 0 through 7 can be used to place the H-Gate by passing n as the parameter for h of the qubit register, indicating which qubit to place the H-Gate on:
q.h(n)
How This Works
When the circuit is executed, there is an approximately equal probability of each possible binary pattern representing the desired number, as shown in this graph of a circuit ran with 1024 shots (number of times it was executed):
Since the decimal values 0 through 7 are 3-digit binary numbers, this method works because binary numbers one through seven only use three bits, as shown below:
[decimal] =[binary]=[3-bit]
1 = 00000001 = 001
2 = 00000010 = 010
3 = 00000011 = 011
4 = 00000100 = 100
5 = 00000101 = 101
6 = 00000110 = 110
7 = 00000111 = 111
The decimal conversion returns a random value for qubits 0 through 7, and this is a true random number and an example of a quantum random number generator (QRND).
Using Different Methods
If there are 25 qubits and a requirement for a random number between 1 and 25, it can still be done with a single call and subsequent bit addition in post-processing, as shown in the example below:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q[24];
creg c[24];
reset q;
h q;
measure q -> c;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
lA = [(k[::-1],v) for k,v in counts.items()]
lA.sort(key = lambda x: x[1], reverse=True)
Y = []
for k, v in lA: Y.append( [ int(c) for c in k ] )
n = 0;
for i in Y[0]:
if i == 1:
n += 1
The H-gate can then be applied to the qubit by passing n as the parameter for h:
q.h(n)
This method works for any number, but keep in mind that you can only generate a number equal to the number of available qubits. So while it can generate a number to represent a random qubit, it will not work for numbers larger than the available qubits which is a significant limitation given the limited quantity of quality and coherent qubits available in the industry currently.
Aside from selecting random qubits, if a number between 0 and 100 is desired, the above assembly code could be executed four times. Upon completion, the resulting four n values from each run can be added together to create a truly random number between 0 and 100, without the need for expensive methods like calling the circuit 100 times.
However, this is the same bit-counting method as described previously, just done more efficiently. There are much more efficient ways to accomplish this.
4-Bits 4 Efficiency
To further improve on efficiency we can simply use 4 qubits or like so:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[4];
h q;
measure q -> c;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
In this case c will return a 4-bit number which can be anywhere from 0 to 15 when converted to decimal using the same process as above. However, if the original 3 qubit registers is coupled with an addition a 4-qubit register, additional opportunities are available to expand the randomization range.
For example, a qubit can be registered as a switch to product a 50/50 chance of being 0-7 or 8-15, 3-bit or 4-bit respectively, as shown in the following code:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q0[3];
qreg q1[4];
qreg q2[1];
creg c0[3];
creg c1[4];
creg c2[1];
reset q0;
reset q1;
reset q2;
h q2;
measure q2 -> c2;
if(c2==0) h q0;
measure q0 -> c0;
if(c2==1) h q1;
measure q1 -> c1;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
This results in a larger circuit:
In this case a switch, represented by random bit c2 from q2, is introduced. If c2 equals 0, then the q0 register is measured (3-bit), otherwise the q1 register is measured (4-bit). The register whose qubits did not have the H-Gate applied will always be 0, however, since each register can also return 0 ({0,0,0} or {0,0,0,0}), the c2 switch is a dependency.
This gives you up 16 numbers, 0 through 7 from the 3-bit number previously discussed, and the following possible 4-bit numbers:
[numeric] = [binary]=[4-bit]
0 = 00000000 = ₀000
1 = 00000001 = ₀001
2 = 00000010 = ₀010
3 = 00000011 = ₀011
4 = 00000100 = ₀100
5 = 00000101 = ₀101
6 = 00000110 = ₀110
7 = 00000111 = ₀111
8 = 00001000 = 1000
9 = 00001001 = 1001
10 = 00001010 = 1010
11 = 00001011 = 1011
12 = 00001100 = 1100
13 = 00001101 = 1101
14 = 00001110 = 1110
15 = 00001111 = 1111
The math changes slightly as well. Based on the q2 measurement, if c2 returns 0 (which is |0> measure from q0), the 3-bit number c0 should be used. However, if it’s 1, the 4-bit c1 is used.
In this case, the value of c0, is established as a set and its inverse assigned to b and the same is done for c1 to c, c0 is the measurement from q0 and c1 from q1.
Thus if c2 equals 0, c0 is used, and if c2 equals 1, c1 is used. This will generate a random number between 0 and 16 with a 50/50 chance of being 0 through 7 or 8 through 15, and only 8 qubits were required from the common 16 used in most examples of a 16-bit QRND circuit.
This can be understood and simplified both programmatically and mathematically by this notation:
Where x = c2 when assigning b as in b = f(c2).
Efficient Randomization Range Expansion
The 3-bit number used in the previous example may seem unnecessary. and for that example, it is. That could have been just as easily and more efficiently done like this:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
h q;
measure q -> c;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
The return counts would be the binary representation of a decimal number between the range of 0 and 15.
However, it was exampled with a 3-bit and 4-bit number as part of the progression leading to the following example because with numbers 0 through 16, there are a variety of ways to expand this range without the need of more qubits. This can be done mathematically, sequentially, programmatically, using concatenation, bit counting and/or a variety of other methods that produce more efficient results than brute force.
Here’s a more complex example that requires a 3-bit and 4-bit number to produce a number range between 0 and 23 using only 8 qubits, which is close to the range of the aforementioned range that required 25 qubits.
Randomizing to 23 Using Only 9 Qubits in 1 Shot
Expanding on the previous returns of 3-bit and 4-bit sets, this example applies an XOR operator to the results of both sets (3-bit and 4-bit) after converting them to decimal values to produce a final decimal value, n.
This method would result in a number between 16 and 22. Thus, a switch can be used to determine whether the 4-bit set should be used or to XOR the two sets for a higher number. Unfortunately, reliance on the previous c2 switch is insufficient and would produce uneven probabilities since there would be an equal chance of 16 through 22 as there would be for 0 through 15. Therefore, to decide on 16 through 22, the c2 switch needs to be modified. To do this, we need to give q2 three opportunities to be 1, and this is accomplished by resetting the q2 register of 1 qubit, then applying the H-Gate to force a state, and finally measuring the qubit (q2) into a classical bit (c2). If that classical bit equals 0, then the process repeats for up to 2 more times and is measured each time. This gives q2 three chances, a 33% chance, of being in a spin state of ket 1, and a subsequent value of c2 being equal to 1.
This may seem confusing since it appears as if c0 could be dual-purposed for this, but the problem there is that c0 reads from q0, which uses three different qubits to form a binary pattern, of which has 8 probable outcomes. This gives us a 12.5% chance of all three being 0, whereas the modification to c2 gives a single qubit 3 chances to become 1, which is a different probability.
With this adjustment, if the condition of c2 equaling 0 is satisfied, c0 XOR c1 should be calculated. The assembly code can be modified for the c2 switch to produce the above requirement with the following code:
from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer
qasm_code = '''
OPENQASM 2.0;
include "qelib1.inc";
qreg q0[3];
qreg q1[4];
qreg q2[1];
qreg q3[1];
creg c0[3];
creg c1[4];
creg c2[1];
creg c3[1];
h q2;
measure q2 -> c2;
if(c2==0) reset q2;
if(c2==0) h q2;
if(c2==0) measure q2 -> c2;
measure q2 -> c2;
if(c2==0) reset q2;
if(c2==0) h q2;
if(c2==0) measure q2 -> c2;
h q0;
measure q0 -> c0;
h q1;
measure q1 -> c1;
h q3;
measure q3 -> c3;
if(c3==0) x q3;
if(c1==0) x q3;
if(c0==1) x q3;
if(c0==2) x q3;
if(c0==3) x q3;
if(c0==4) x q3;
if(c0==5) x q3;
if(c0==6) x q3;
if(c0==7) x q3;
measure q3 -> c3;
'''
circuit = QuantumCircuit.from_qasm_str(qasm_code)
backend = Aer.get_backend('[processor]')
job = execute(circuit, backend, shots=1)
result = job.result()
counts = result.get_counts(circuit)
Unfortunately, the only operator of an [ if ] condition in QASM is equality. In classical ASM you can do this more easily with the [ JNE ] conditional operator as shown below:
CMP C0, 00 ; compare C0 to 00
JNE L1 ; if equality is false, jump to 1
L1: ; bit flip q3
This is not an option in QASM, so each bit has to be evaluated:
if(c0==1) x q3;
if(c0==2) x q3;
if(c0==3) x q3;
if(c0==4) x q3;
if(c0==5) x q3;
if(c0==6) x q3;
if(c0==7) x q3;
measure q3 -> c3;
The quantum circuit would look like this:
This measures q0 and q1 and returns both c0 and c1 regardless of c2, and a c3 switch has been added. This is because this process does not produce a range between 0 and 22; it produces a range between 0 and 23. The 33% chance of being 0 through 22 is uneven because 16-22 has 7 possibilities, while 0–7 and 8–15 each have 8 possibilities. This gives each number from 16 to 22 a ½ percent advantage over 0 through 15.
This is resolved under the condition c0 and c1 are both 0, under which condition the number 23 is automatically the result. This is decided by the c3 switch that looks at c1 and performs an X-Gate (bit-flip gate) on q1 if c1 is 0. Subsequently, c0 is evaluated by reading the integer value if binary numbers 000 to 111 (int 1 to 7), and if any of these conditions are true, an additional X-Gate is applied to q3, which is then measured into c3. This can be mathematically described as:
Thus, if c3 equals 1, the number is 23. This is done because normally the chance of zero between out of 15 is 6.25%. However, in introducing the 33% chance of being 16 to 22, the condition of c0==0 and c1==0 gives the number zero a 12.5% since it can happen under either condition of c0 due to the fact that 0 XOR 0 = 0. That means that (c0 ≠ 0 & c1==0) is equal to 0, and (c0 = 0 & c1==0) is also equal to 0. For this reason, if (c0==0 & c1==0) as indicated by c3, this overrides the XOR instruction, and the result is 23.
Alternatively, the result of 23 can be omitted for the range of 0 to 22.
However, in this case, the c3 switch can also be omitted, and 23 can be identified when (c0==0 & c1==0) regardless of c2 because it no longer cares about the equal probability of 23. Without the c3 switch, there is a little over a 4% chance of 23, while 0 to 22 each have equal chances of 6.25%. Since we don’t care about the chance of 23 anymore, it would just result in a do-over where the circuit is executed a second time.
The foregoing generates a random number between the range of 0 to 23 using only 9 qubits in one execution of the quantum circuit, or a random number between the range of 0 to 22 with 8 qubits, with a 4.15% chance of two executions of the quantum circuit.
Further Processing
Further, the result of the XOR could be converted back to binary resulting in a 16-number for further processing or operation like so:
Where b = c0 and c = c1. The above converts binary sets b and c into decimals, applies an XOR operator, then converts x back into a binary number to result in a 16-bit number.
There are a number of different ways the base values can be exploited, the above examples just a few, but the point overall is that efficiency can be achieved mathematically and programmatically without putting and unnecessary burden on hardware or writing copious amounts of assembly code.
The Pseudorandom Way
If you landed on this article just wanting to know how to do a simple random number in Python and apply an H-Gate to its qubit counter part, I haven’t forgotten about you. If the previous method is too complicated or if a pseudorandom number is sufficient for the requirements, there is a very simple way to handle this using Python and Jupyter Notebook.
Import the random library and use Python to create a pseudorandom number on the fly, then pass that value as the parameter for h:
import random
n = random.randint(0,5)
qc = QuantumCircuit(6)
qc.h(n)
That’s it.
Summary
Note: In the earlier examples where “[processor]” is indicated, it should be replaced with the identifier of the backend server hosting the quantum processor.
Quantum processors have the ability to generate truly random numbers using quantum circuits, processors, and qubits due to the inherent nature of entangled particles’ physical states. By applying an H-Gate to a qubit and measuring it into classical bits, the interpretation of a spin-up state |0> (ket 0) corresponding to a binary 0, and a spin-down state |1> (ket 1) corresponding to a binary 1, can be of practical use (such as that of placing an H-Gate on a random qubit).
Many examples commonly rely on brute force methods of repetitive circuit executions, which can be expensive, equal to the upper limit of the desired random number. However, with some creativity and basic mathematics, the measurements can be interpreted in various ways to represent the same numbers without the need for excessive circuit executions.
Developing efficient circuits for quantum processors, regardless of the instruction set, is crucial, similar to optimizing classical microprocessors. Inefficient circuits result in unnecessary resource consumption and reduced performance. The foregoing examples, demonstrate various ways to refine inefficient circuits commonly used with current quantum processors and simulators, by replacing it with more efficient circuits that require only a fraction of the time and resources compared to the common inefficient methods.
Moreover, this is done with limited complexity.
Enjoy.
References