I'm working on a Quantum Random Number Generator and wanted to get some feedback on the project so far.
I'm using PyQuil to generate machine code for the quantum computer, first to create a bell state placing our random bit into a superposition, then I measure the bit to collapse the superposition and repeat the process for N bits.
I'm a little concerned because I haven't been able to come across much data in terms of how long it takes to restore a qubit to a superposition so I can't really say how fast this program is going to work, but on my virtual quantum computer it runs okayish,~0.5 seconds to generate 512 bits, ~1 second for 1024 bits, ~2.09 seconds for 2048 bits.
The QRandom class is a subclass of the Random() class, figured it was easier to re-use than reinvent the wheel completely.
qrandom.py:
"""
Random variable generator using quantum machines
"""
from math import sqrt as _sqrt
import random
import psutil
from pyquil.quil import Program
from pyquil.api import get_qc
from pyquil.gates import H, CNOT
import vm
__all__ = ["QRandom", "random", "randint", "randrange", "getstate", "setstate", "getrandbits"]
BPF = 53 # Number of bits in a float
RECIP_BPF = 2**-BPF
def bell_state():
"""Returns the Program object of a bell state operation on a quantum computer
"""
return Program(H(0), CNOT(0, 1))
def arr_to_int(arr):
"""returns an integer from an array of binary numbers
arr = [1 0 1 0 1 0 1] || [1,0,1,0,1,0,1]
"""
return int(''.join([str(i) for i in arr]), 2)
def arr_to_bits(arr):
return ''.join([str(i) for i in arr])
def int_to_bytes(k, x=64):
"""returns a bytes object of the integer k with x bytes"""
#return bytes(k,x)
return bytes(''.join(str(1 & int(k) >> i) for i in range(x)[::-1]), 'utf-8')
def bits_to_bytes(k):
"""returns a bytes object of the bitstring k"""
return int(k, 2).to_bytes((len(k) + 7) // 8, 'big')
def qvm():
"""Returns the quantum computer or virtual machine"""
return get_qc('9q-square-qvm')
def test_quantum_connection():
"""
Tests the connection to the quantum virtual machine.
attempts to start the virtual machine if possible
"""
while True:
qvm_running = False
quilc_running = False
for proc in psutil.process_iter():
if 'qvm' in proc.name().lower():
qvm_running = True
elif 'quilc' in proc.name().lower():
quilc_running = True
if qvm_running is False or quilc_running is False:
try:
vm.start_servers()
except Exception as e:
raise Exception(e)
else:
break
class QRandom(random.Random):
"""Quantum random number generator
Generates a random number by collapsing bell states on a
quantum computer or quantum virtual machine.
"""
def __init__(self):
super().__init__(self)
self.p = bell_state()
self.qc = qvm()
# Make sure we can connect to the servers
test_quantum_connection()
def random(self):
"""Get the next random number in the range [0.0, 1.0)."""
return (int.from_bytes(self.getrandbits(56, 'bytes'), 'big') >> 3) * RECIP_BPF
def getrandbits(self, k, x="int"):
"""getrandbits(k) -> x. generates an integer with k random bits"""
if k <= 0:
raise ValueError("Number of bits should be greater than 0")
if k != int(k):
raise ValueError("Number of bits should be an integer")
out = bits_to_bytes(arr_to_bits(self.qc.run_and_measure(self.p, trials=k)[0]))
if x in ('int', 'INT'):
return int.from_bytes(out, 'big')
elif x in ('bytes', 'b'):
return out
else:
raise ValueError(str(x) + ' not a valid type (int, bytes)')
def _test_generator(n, func, args):
import time
print(n, 'times', func.__name__)
total = 0.0
sqsum = 0.0
smallest = 1e10
largest = -1e10
t0 = time.time()
for i in range(n):
x = func(*args)
total += x
sqsum = sqsum + x*x
smallest = min(x, smallest)
largest = max(x, largest)
t1 = time.time()
print(round(t1 - t0, 3), 'sec,', end=' ')
avg = total/n
stddev = _sqrt(sqsum / n - avg*avg)
print('avg %g, stddev %g, min %g, max %g\n' % \
(avg, stddev, smallest, largest))
def _test(N=2000):
_test_generator(N, random, ())
# Create one instance, seeded from current time, and export its methods
# as module-level functions. The functions share state across all uses
#(both in the user's code and in the Python libraries), but that's fine
# for most programs and is easier for the casual user than making them
# instantiate their own QRandom() instance.
_inst = QRandom()
#seed = _inst.seed
random = _inst.random
randint = _inst.randint
randrange = _inst.randrange
getstate = _inst.getstate
setstate = _inst.setstate
getrandbits = _inst.getrandbits
if __name__ == '__main__':
_test(2)
vm.py
import os
def start_servers():
try:
os.system("gnome-terminal -e 'qvm -S'")
os.system("gnome-terminal -e 'quilc -S'")
except:
try:
os.system("terminal -e 'qvm -S'")
os.system("terminal -e 'quilc -S'")
except:
exit()
2 Answers 2
Interesting stuff. Some high-level comments:
- calling
exit()
when something goes wrong is fine for one-off code of your own but not very polite if you're making a library for others to use. Raise a well-named exception with a meaningful error message instead. As it is, since you're instantiatingQRandom
at the module level, if someone even imports your module and they don't have qvm/quilc installed (or if they're on a mac, which has neithergnome-terminal
norterminal
!) their code will silently exit. - exporting
getstate
andsetstate
fromrandom.Random
seems a bit misleading here, since they won't work as expected. I would override them to raiseNotImplementedError
unless you have a way of implementing them - and the same forseed
andjumpahead
, in fact.
I have a few more minor detail comments about the code but I'll have to add those later - if you're interested, anyway.
-
\$\begingroup\$ I'd love to hear more, I'm looking into modifying start_servers() to be platform independent, so if you have any pointers I'm all ears! As for get/setstate and seed I'm not sure whether to implement those or not, I'm still kinda iffy on it since in theory we can control the range the qubit is in and it doesn't have to be in a |0⟩ + i |1⟩ superposition, but I have overridden them as suggested. Thanks for the help :) \$\endgroup\$Noah Wood– Noah Wood2019年04月03日 06:23:11 +00:00Commented Apr 3, 2019 at 6:23
You define several helper functions out of which some are unused and some seem only related to your personal usage, so not really part of a library: qvm
and bell_state
should be better directly integrated into the QRandom
constructor.
I’m also not found of your test_quantum_connection
; at least let the user decide if they want to run it or not. And since you’re not doing anything with the exception you are catching, you'd be better off removing the try
completely.
Your _test_generator
function feels really wrong, to me. At the very least, use time.perf_counter
instead of time.time
. But ultimately you should switch to timeit
.
Speaking of which, read the note about timing repeated tests:
Note: It’s tempting to calculate mean and standard deviation from the result vector and report these. However, this is not very useful. In a typical case, the lowest value gives a lower bound for how fast your machine can run the given code snippet; higher values in the result vector are typically not caused by variability in Python’s speed, but by other processes interfering with your timing accuracy. So the min() of the result is probably the only number you should be interested in. After that, you should look at the entire vector and apply common sense rather than statistics.
I, however, would remove this function entirely and perform timing tests from the command line directly:
$ python3 -m timeit -s 'from qrandom import random' 'random()'
Your various conversion methods seems unefficient: arr_to_int
for instance feeds strings to int
rather than performing simple additions and bit shifts. Compare:
>>> def stringifier(arr):
... return int(''.join([str(i) for i in arr]), 2)
...
>>> def mapper(arr):
... return int(''.join(map(str, arr)), 2)
...
>>> def adder(arr):
... return sum(v << i for i, v in enumerate(reversed(arr)))
...
>>> from functools import reduce
>>> def add_bit(number, bit):
... return (number << 1) + bit
...
>>> def reducer(arr):
... return reduce(add_bit, arr, 0)
...
>>> for name in ['stringifier', 'mapper', 'adder', 'reducer']:
... elapsed = timeit.repeat('{}(lst)'.format(name), 'from __main__ import {}; lst=[1,0,1,0,1,0,1,1,1,0,0,0,1,1,0,1,0]'.format(name), repeat=10)
... print(name, ':', min(elapsed))
...
stringifier : 2.625876844045706
mapper : 2.1048526159720495
adder : 1.908082987065427
reducer : 1.8361501740291715
Your are also performing too much conversions in random
since you ask for bytes just to convert them to integers right away. Why don't you convert directly to integer then? Besides, this should be the return type of getrandbits
anyway; I see little gain in adding the "select your return type" overhead and complexity.
Proposed improvements:
"""
Random variable generator using quantum machines
"""
import random
from functools import reduce
from pyquil.quil import Program
from pyquil.api import get_qc
from pyquil.gates import H, CNOT
__all__ = ["QRandom", "random", "randint", "randrange", "getstate", "setstate", "getrandbits"]
BPF = 53 # Number of bits in a float
RECIP_BPF = 2**-BPF
class QRandom(random.Random):
"""Quantum random number generator
Generates a random number by collapsing bell states on a
quantum computer or quantum virtual machine.
"""
def __init__(self):
super().__init__(self, computer_name='9q-square-qvm', check_connection=False)
self.p = Program(H(0), CNOT(0, 1))
self.qc = get_qc(computer_name)
if check_connection:
test_quantum_connection()
def random(self):
"""Get the next random number in the range [0.0, 1.0)."""
return (self.getrandbits(56) >> 3) * RECIP_BPF
def getrandbits(self, k, x='int'):
"""getrandbits(k) -> x. generates an integer with k random bits"""
if k <= 0:
raise ValueError("Number of bits should be greater than 0")
trials = int(k)
if k != trials:
raise ValueError("Number of bits should be an integer")
bitfield = self.qc.run_and_measure(self.p, trials=trials)[0]
result = reduce(_add_bits, bitfield, 0)
if x.lower() in ('int', 'i'):
return result
elif x.lower() in ('bytes', 'b'):
return result.to_bytes((result.bit_length() + 7) // 8, 'big')
else:
raise ValueError(str(x) + ' not a valid type (int, bytes)')
# Create one instance, seeded from current time, and export its methods
# as module-level functions. The functions share state across all uses
#(both in the user's code and in the Python libraries), but that's fine
# for most programs and is easier for the casual user than making them
# instantiate their own QRandom() instance.
_inst = QRandom()
#seed = _inst.seed
random = _inst.random
randint = _inst.randint
randrange = _inst.randrange
getstate = _inst.getstate
setstate = _inst.setstate
getrandbits = _inst.getrandbits
def _add_bit(number, bit):
return (number << 1) + bit
def test_quantum_connection():
"""
Tests the connection to the quantum virtual machine.
attempts to start the virtual machine if possible
"""
import vm
import psutil
qvm_running = False
quilc_running = False
while True:
for proc in psutil.process_iter():
name = proc.name().lower()
if 'qvm' in name:
qvm_running = True
elif 'quilc' in name:
quilc_running = True
if not qvm_running or not quilc_running:
vm.start_servers()
else:
break
def _test_generator(function_name, *arguments, amount=1000000):
import timeit
return min(timeit.repeat(
'{}{}'.format(function_name, arguments),
'from {} import {}'.format(__name__, function_name),
number=amount))
if __name__ == '__main__':
_test_generator('random')
I kept _test_generator
and the bottom half of getrandbits
for completeness but I still advise to remove them if you plan on releasing it as a library.
Explore related questions
See similar questions with these tags.