Comparing Classical and Quantum Finite Automata (QFA)

Finite Automaton has been a mathematical model for computation since its invention in the 1940s. The purpose of a Finite State Machine is to recognize patterns within an input taken from some character set and accept or reject the input based on whether the pattern defined by the machine occurs in the input. The machine requires a list of states, the initial state, and the conditions for each transition from state to state. Such classical examples are vending machines, coin-operated turnstiles, elevators, traffic lights, etc.

In the classical algorithm, the sequence begins in the start state, and will only make a transition if the next character in the input string matches the label on the transition from the current to the next state. The machine will continue making transitions on each input character until no move is possible. The string will be accepted if its final state is in the accept state and will be rejected if its final state is anywhere else.

As for Quantum Finite Automata (QFA), the machine works by accepting a finite-length string of letters from a finite alphabet and utilizing quantum properties such as superposition to assign the string a probability of being in either the accept or reject state.


Contributors

Kaitlin Gili, Rudy Raymond

Prime Divisibility Algorithm

Let's say that we have a string with $ a^i $ letters and we want to know whether the string is in the language $ L $ where $ L $ = {$ a^i $ | $ i $ is divisble by $ p $} and $ p $ is a prime number. If $ i $ is divisible by $ p $, we want to accept the string into the language, and if not, we want to reject it. $|0\rangle $ and $ |1\rangle $ serve as our accept and reject states.

Classically, this algorithm requires a minimum of $ log(p) $ bits to store the information, whereas the quantum algorithm only requires $ log(log(p)) $ qubits. For example, using the highest known prime integer, the classical algorithm requires a minimum of 77,232,917 bits, whereas the quantum algorithm only requires 27 qubits.

Introduction

The algorithm in this notebook follows that in Ambainis et al. 1998. We assume that we are given a string and a prime integer. If the user does not input a prime number, the output will be a ValueError. First, we demonstrate a simpler version of the quantum algorithm that uses $ log(p) $ qubits to store the information. Then, we can use this to more easily understand the quantum algorithm that requires only $ log(log(p)) $ qubits.

The Algorithm for Log(p) Qubits

The algorithm is quite simple as follows.

  1. Prepare quantum and classical registers for $ log(p) $ qubits initialized to zero. $$ |0\ldots 0\rangle $$
  2. Prepare $ log(p) $ random numbers k in the range {$ 1 $... $ p-1 $}. These numbers will be used to decrease the probability of a string getting accepted when $ i $ does not divide $ p $.
  3. Perform a number of $ i $ Y-Rotations on each qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k}{p} $$
  4. In the final state: $$ \cos \theta |0\rangle + \sin \theta |1\rangle $$ $$ \theta = \frac{2 \pi k} p {i} $$
  5. Measure each of the qubits in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every qubit and the state will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

The Circuit

We now implement the QFA Prime Divisibility algorithm with QISKit by first preparing the environment.


In [1]:
# useful additional packages 
import random
import math
from sympy.ntheory import isprime

# importing QISKit
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, IBMQ, execute

from qiskit.wrapper.jupyter import *
from qiskit.backends.ibmq import least_busy

from qiskit.tools.visualization import matplotlib_circuit_drawer as circuit_drawer
from qiskit.tools.visualization import plot_histogram, qx_color_scheme

In [2]:
IBMQ.load_accounts()
sim_backend = Aer.get_backend('qasm_simulator')
device_backend = least_busy(IBMQ.backends(operational=True, simulator=False))
device_coupling = device_backend.configuration()['coupling_map']

We then use QISKit to program the algorithm.


In [3]:
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(prime))) #Rounds up to the next integer of the log(prime)
        qr = QuantumRegister(n) #Creates a quantum register of length log(prime) for log(prime) qubits
        cr = ClassicalRegister(n) #Creates a classical register for measurement 
        qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for x in range(n): #For each qubit, we want to apply a series of unitary operations with a random int
            random_value = random.randint(1,prime - 1) #Generates the random int for each qubit from {1, prime -1}
            for letter in string: #For each letter in the string, we want to apply the same unitary operation to each qubit
                qfaCircuit.ry((2*math.pi*random_value) / prime, qr[x]) #Applies the Y-Rotation to each qubit 
            qfaCircuit.measure(qr[x], cr[x]) #Measures each qubit 
        return qfaCircuit #Returns the created quantum circuit

The qfa_algorithm function returns the Quantum Circuit qfaCircuit.

Experiment with Simulators

We can run the above circuit on the simulator.


In [4]:
#A function that returns a string saying if the string is accepted into the language or rejected
def accept(parameter):
    states = list(result.get_counts(parameter))
    for s in states:
        for integer in s:
            if integer == "1":
                return "Reject: the string is not accepted into the language"
    return "Accept: the string is accepted into the language"

Insert your own parameters and try even larger prime numbers.


In [5]:
range_lower = 0
range_higher = 36
prime_number = 11

In [6]:
for length in range(range_lower,range_higher):
    params = qfa_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))


Accept: the string is accepted into the language 
 Length: 0   {'000': 1000}
Reject: the string is not accepted into the language 
 Length: 1   {'001': 4, '010': 2, '011': 14, '100': 32, '101': 138, '110': 124, '111': 686}
Reject: the string is not accepted into the language 
 Length: 2   {'000': 6, '001': 14, '010': 125, '011': 571, '100': 1, '101': 6, '110': 40, '111': 237}
Reject: the string is not accepted into the language 
 Length: 3   {'000': 8, '001': 453, '010': 7, '011': 194, '100': 1, '101': 229, '110': 2, '111': 106}
Reject: the string is not accepted into the language 
 Length: 4   {'001': 34, '010': 1, '011': 140, '100': 2, '101': 138, '110': 17, '111': 668}
Reject: the string is not accepted into the language 
 Length: 5   {'000': 202, '001': 92, '010': 271, '011': 143, '100': 85, '101': 35, '110': 126, '111': 46}
Reject: the string is not accepted into the language 
 Length: 6   {'000': 55, '001': 22, '010': 264, '011': 112, '100': 56, '101': 21, '110': 326, '111': 144}
Reject: the string is not accepted into the language 
 Length: 7   {'000': 1, '001': 7, '010': 2, '011': 11, '100': 79, '101': 370, '110': 103, '111': 427}
Reject: the string is not accepted into the language 
 Length: 8   {'000': 227, '001': 85, '010': 93, '011': 38, '100': 283, '101': 120, '110': 109, '111': 45}
Reject: the string is not accepted into the language 
 Length: 9   {'000': 107, '001': 38, '010': 546, '011': 214, '100': 12, '101': 6, '110': 51, '111': 26}
Reject: the string is not accepted into the language 
 Length: 10   {'000': 267, '001': 115, '010': 372, '011': 172, '100': 23, '101': 9, '110': 29, '111': 13}
Accept: the string is accepted into the language 
 Length: 11   {'000': 1000}
Reject: the string is not accepted into the language 
 Length: 12   {'000': 3, '001': 7, '011': 7, '100': 109, '101': 582, '110': 55, '111': 237}
Reject: the string is not accepted into the language 
 Length: 13   {'000': 56, '001': 259, '010': 66, '011': 325, '100': 14, '101': 108, '110': 29, '111': 143}
Reject: the string is not accepted into the language 
 Length: 14   {'000': 3, '001': 1, '010': 319, '011': 122, '100': 7, '101': 2, '110': 385, '111': 161}
Reject: the string is not accepted into the language 
 Length: 15   {'000': 1, '001': 1, '010': 170, '011': 7, '100': 8, '101': 1, '110': 752, '111': 60}
Reject: the string is not accepted into the language 
 Length: 16   {'000': 22, '001': 93, '010': 10, '011': 34, '100': 113, '101': 504, '110': 38, '111': 186}
Reject: the string is not accepted into the language 
 Length: 17   {'000': 22, '001': 85, '010': 7, '011': 32, '100': 115, '101': 474, '110': 45, '111': 220}
Reject: the string is not accepted into the language 
 Length: 18   {'000': 118, '001': 172, '010': 55, '011': 82, '100': 181, '101': 214, '110': 73, '111': 105}
Reject: the string is not accepted into the language 
 Length: 19   {'000': 255, '001': 393, '010': 108, '011': 154, '100': 25, '101': 42, '110': 8, '111': 15}
Reject: the string is not accepted into the language 
 Length: 20   {'000': 2, '001': 104, '010': 2, '011': 43, '100': 13, '101': 579, '110': 7, '111': 250}
Reject: the string is not accepted into the language 
 Length: 21   {'000': 286, '001': 24, '010': 357, '011': 35, '100': 125, '101': 9, '110': 150, '111': 14}
Accept: the string is accepted into the language 
 Length: 22   {'000': 1000}
Reject: the string is not accepted into the language 
 Length: 23   {'000': 57, '001': 10, '010': 334, '011': 34, '100': 70, '101': 9, '110': 449, '111': 37}
Reject: the string is not accepted into the language 
 Length: 24   {'000': 29, '001': 130, '010': 2, '011': 7, '100': 149, '101': 621, '110': 14, '111': 48}
Reject: the string is not accepted into the language 
 Length: 25   {'000': 131, '001': 48, '010': 190, '011': 69, '100': 169, '101': 77, '110': 237, '111': 79}
Reject: the string is not accepted into the language 
 Length: 26   {'000': 3, '001': 303, '010': 1, '011': 126, '100': 8, '101': 400, '110': 2, '111': 157}
Reject: the string is not accepted into the language 
 Length: 27   {'000': 14, '001': 487, '010': 7, '011': 196, '100': 2, '101': 211, '110': 2, '111': 81}
Reject: the string is not accepted into the language 
 Length: 28   {'000': 1, '001': 4, '010': 10, '011': 5, '100': 65, '101': 109, '110': 354, '111': 452}
Reject: the string is not accepted into the language 
 Length: 29   {'000': 189, '001': 299, '010': 77, '011': 106, '100': 100, '101': 135, '110': 45, '111': 49}
Reject: the string is not accepted into the language 
 Length: 30   {'000': 10, '001': 55, '010': 72, '011': 276, '100': 16, '101': 90, '110': 67, '111': 414}
Reject: the string is not accepted into the language 
 Length: 31   {'000': 4, '010': 170, '011': 13, '100': 19, '110': 723, '111': 71}
Reject: the string is not accepted into the language 
 Length: 32   {'000': 102, '001': 5, '010': 52, '011': 6, '100': 537, '101': 47, '110': 229, '111': 22}
Accept: the string is accepted into the language 
 Length: 33   {'000': 1000}
Reject: the string is not accepted into the language 
 Length: 34   {'001': 23, '010': 14, '011': 876, '101': 1, '111': 86}
Reject: the string is not accepted into the language 
 Length: 35   {'000': 9, '001': 3, '010': 7, '011': 2, '100': 495, '101': 184, '110': 204, '111': 96}

Drawing the circuit of the QFA

Below is the snapshop of the QFA for reading the bitstring of length $3$. It can be seen that there are independent QFAs each of which performs $Y$ rotation for $3$ times.


In [7]:
circuit_drawer(qfa_algorithm("a"* 3, prime_number), style=qx_color_scheme())


The Algorithm for Log(Log(p)) Qubits

The algorithm is quite simple as follows.

  1. Prepare a quantum register for $ log(log(p)) + 1 $ qubits initialized to zero. The $ log(log(p))$ qubits will act as your control bits and the 1 extra will act as your target bit. Also prepare a classical register for 1 bit to measure the target. $$ |0\ldots 0\rangle |0\rangle $$
  2. Hadamard the control bits to put them in a superposition so that we can perform multiple QFA's at the same time.
  3. For each of $s $ states in the superposition, we can perform an individual QFA with the control qubits acting as the random integer $ k $ from the previous algorithm. Thus, we need $ n $ values from $ 1... log(p)$ for $ k $. For each letter $ i $ in the string, we perform a controlled y-rotation on the target qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k_{s}}{p} $$
  4. The target qubit in the final state: $$ \cos \theta |0\rangle + \sin \theta |1\rangle $$ $$ \theta = \sum_{s=0}^n \frac{2 \pi k_{s}} p {i} $$
  5. Measure the target qubit in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every QFA and the state of the target will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

The Circuit

We then use QISKit to program the algorithm.


In [8]:
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_controlled_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(math.log(prime,2),2))) #Represents log(log(p)) control qubits 
        states = 2 ** (n) #Number of states that the qubits can represent/Number of QFA's to be performed 
        qr = QuantumRegister(n+1) #Creates a quantum register of log(log(prime)) control qubits + 1 target qubit
        cr = ClassicalRegister(1) #Creates a classical register of log(log(prime)) control qubits + 1 target qubit
        control_qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for q in range(n): #We want to take each control qubit and put them in a superposition by applying a Hadamard Gate
            control_qfaCircuit.h(qr[q])
        for letter in string: #For each letter in the string, we want to apply a series of Controlled Y-rotations
            for q in range(n):  
                control_qfaCircuit.cu3(2*math.pi*(2**q)/prime, 0, 0, qr[q], qr[n]) #Controlled Y on Target qubit 
        control_qfaCircuit.measure(qr[n], cr[0]) #Measure the target qubit  
        return control_qfaCircuit #Returns the created quantum circuit

The qfa_algorithm function returns the Quantum Circuit control_qfaCircuit.

Experiment with Simulators

We can run the above circuit on the simulator.


In [9]:
for length in range(range_lower,range_higher):
    params = qfa_controlled_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))


Accept: the string is accepted into the language 
 Length: 0   {'0': 1000}
Reject: the string is not accepted into the language 
 Length: 1   {'0': 782, '1': 218}
Reject: the string is not accepted into the language 
 Length: 2   {'0': 488, '1': 512}
Reject: the string is not accepted into the language 
 Length: 3   {'0': 546, '1': 454}
Reject: the string is not accepted into the language 
 Length: 4   {'0': 643, '1': 357}
Reject: the string is not accepted into the language 
 Length: 5   {'0': 554, '1': 446}
Reject: the string is not accepted into the language 
 Length: 6   {'0': 527, '1': 473}
Reject: the string is not accepted into the language 
 Length: 7   {'0': 661, '1': 339}
Reject: the string is not accepted into the language 
 Length: 8   {'0': 539, '1': 461}
Reject: the string is not accepted into the language 
 Length: 9   {'0': 475, '1': 525}
Reject: the string is not accepted into the language 
 Length: 10   {'0': 758, '1': 242}
Accept: the string is accepted into the language 
 Length: 11   {'0': 1000}
Reject: the string is not accepted into the language 
 Length: 12   {'0': 747, '1': 253}
Reject: the string is not accepted into the language 
 Length: 13   {'0': 451, '1': 549}
Reject: the string is not accepted into the language 
 Length: 14   {'0': 549, '1': 451}
Reject: the string is not accepted into the language 
 Length: 15   {'0': 629, '1': 371}
Reject: the string is not accepted into the language 
 Length: 16   {'0': 554, '1': 446}
Reject: the string is not accepted into the language 
 Length: 17   {'0': 535, '1': 465}
Reject: the string is not accepted into the language 
 Length: 18   {'0': 688, '1': 312}
Reject: the string is not accepted into the language 
 Length: 19   {'0': 531, '1': 469}
Reject: the string is not accepted into the language 
 Length: 20   {'0': 473, '1': 527}
Reject: the string is not accepted into the language 
 Length: 21   {'0': 782, '1': 218}
Accept: the string is accepted into the language 
 Length: 22   {'0': 1000}
Reject: the string is not accepted into the language 
 Length: 23   {'0': 765, '1': 235}
Reject: the string is not accepted into the language 
 Length: 24   {'0': 467, '1': 533}
Reject: the string is not accepted into the language 
 Length: 25   {'0': 519, '1': 481}
Reject: the string is not accepted into the language 
 Length: 26   {'0': 632, '1': 368}
Reject: the string is not accepted into the language 
 Length: 27   {'0': 548, '1': 452}
Reject: the string is not accepted into the language 
 Length: 28   {'0': 528, '1': 472}
Reject: the string is not accepted into the language 
 Length: 29   {'0': 629, '1': 371}
Reject: the string is not accepted into the language 
 Length: 30   {'0': 544, '1': 456}
Reject: the string is not accepted into the language 
 Length: 31   {'0': 485, '1': 515}
Reject: the string is not accepted into the language 
 Length: 32   {'0': 765, '1': 235}
Accept: the string is accepted into the language 
 Length: 33   {'0': 1000}
Reject: the string is not accepted into the language 
 Length: 34   {'0': 784, '1': 216}
Reject: the string is not accepted into the language 
 Length: 35   {'0': 463, '1': 537}

Drawing the circuit of the QFA

Below is the snapshot of the QFA for reading the bitstring of length $3$. It can be seen that there is a superposition of QFAs instead of independent QFAs.


In [10]:
circuit_drawer(qfa_controlled_algorithm("a"* 3, prime_number), style=qx_color_scheme())


Experimenting with Real Devices

Real-device backends have errors and if the above QFAs are executed on the noisy backends, errors in rejecting strings that should have been accepted can happen. Let us see how well the real-device backends can realize the QFAs.

Let us look an example when the QFA should reject the bitstring because the length of the bitstring is not divisible by the prime number.


In [11]:
prime_number = 3
length = 2          # set the length so that it is not divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa1 = qfa_controlled_algorithm("a"* length, prime_number)


The length of a is 2  while the prime number is 3

In [12]:
%%qiskit_job_status
HTMLProgressBar()
job = execute(qfa1, backend=device_backend, coupling_map=device_coupling, shots=100)



In [13]:
result = job.result()
plot_histogram(result.get_counts())


In the above, we can see that the probability of observing "1" is quite significant. Let us see how the circuit looks like.


In [14]:
circuit_drawer(qfa1, style=qx_color_scheme())


Now, let us see what happens when the QFAs should accept the input string.


In [15]:
print_number = length = 3 # set the length so that it is divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa2 = qfa_controlled_algorithm("a"* length, prime_number)


The length of a is 3  while the prime number is 3

In [16]:
%%qiskit_job_status
HTMLProgressBar()
job = execute(qfa2, backend=device_backend, coupling_map=device_coupling, shots=100)



In [17]:
result = job.result()
plot_histogram(result.get_counts())


The error of rejecting the bitstring is equal to the probability of observing "1" which can be checked from the above histogram. We can see that the noise of real-device backends prevents us to have a correct answer. It is left as future work on how to mitigate errors of the backends in the QFA models.


In [18]:
circuit_drawer(qfa2, style=qx_color_scheme())