6
\$\begingroup\$

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()
\$\endgroup\$
1
\$\begingroup\$

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 instantiating QRandom 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 neither gnome-terminal nor terminal!) their code will silently exit.
  • exporting getstate and setstate from random.Random seems a bit misleading here, since they won't work as expected. I would override them to raise NotImplementedError unless you have a way of implementing them - and the same for seed and jumpahead, 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.

\$\endgroup\$
  • \$\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 Apr 3 at 6:23
1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.