Can I implement a full shuffler of a number of arbitrary 8-bit values?
Published
January 1, 2022
In the last post I ended by coming up with an algorithm that would randomly select one out of N qubits. Given this we could actually implement a shuffling algorithm which would be my first real quantum algorithm.
The principle of the algorithm is this:
For 0..N values
Pick one index and swap that with index 0
Consider the values 1..N and repeat
It should be possible to implement this as an entire quantum program that does not measure any internal state until the very end.
Initialization
This works with 8-bit values, so I need a way to initialize them. What I can do is allow the “program” to occupy the high qubits and just copy the bit representation of the ints over to the low bits.
It’s easy enough to do this using the initialize method which can set multiple qubits at once. Let’s try that with a fixed binary string:
Simulation failed and returned the following error message:
ERROR: Failed to load qobj: Unable to cast Python instance to C++ type (compile in debug mode for details)
QiskitError: 'Data for experiment "circuit-0" could not be found.'
Boo, boo. This looks like a bug as the underlying issue is the failure to load the qobj.
Code
from qiskit import QuantumCircuit, Aer, executeqc = QuantumCircuit(4, 2)qc.initialize([float(bit) for bit in"0101"])backend = Aer.get_backend('qasm_simulator')job = execute(qc, backend, memory=True)job.result().get_counts(qc)
QiskitError: 'Sum of amplitudes-squared does not equal one.'
The problem here is that it is tying together these separate qubits into a single initialization. It’s easier to just flip the bits that are set:
from qiskit import QuantumCircuitvalue =42# 00101010# get the value, which looks like 0b101010# then reverse it so that enumerate matches bit indexbinary_value =bin(value)[2:][::-1]qc = QuantumCircuit(8, 8)for index, value inenumerate(binary_value):if value =="1": qc.x(index)qc.barrier() # makes it render nicelyqc.measure(range(8), range(8))display(qc.draw(output="mpl"))binary_result = run(qc, shots=1).get_memory()[0]print(f"output: {binary_result}")print(f"as int: {int(binary_result, base=2)}")
output: 00101010
as int: 42
That was more painful than I expected. Anyway with this done I can initialize the qubits and read the results. Let’s try unconditionally swapping two values.
Swap Gate
There is an operation available on the Quantum Circuit called swap which is associated with the SwapGate. It does exactly what it sounds like - it swaps the two states of the provided qubits without involving an intermediate qubit. The mechanics of this is interesting however I’m more focused on implementing this algorithm right now.
Code
from qiskit import QuantumCircuitvalue_1 =42value_2 =69binary_value_1 =bin(value_1)[2:][::-1]binary_value_2 =bin(value_2)[2:][::-1]qc = QuantumCircuit(16, 16)for index, value inenumerate(binary_value_1):if value =="1": qc.x(index)for index, value inenumerate(binary_value_2, start=8):if value =="1": qc.x(index)for left_qubit, right_qubit inenumerate(range(8, 16)): qc.swap(qubit1=left_qubit, qubit2=right_qubit)qc.measure(range(16), range(16))# the image is getting really big# display(qc.draw(output="mpl"))# the memory is arranged from high to low so the first 8 characters correspond to qubits 8-15binary_result = run(qc, shots=1).get_memory()[0]binary_result_1 = binary_result[8:]print(f"input 1: {value_1}")print(f"output 1: {binary_result_1}")print(f"as int 1: {int(binary_result_1, base=2)}")print()binary_result_2 = binary_result[:8]print(f"input 2: {value_2}")print(f"output 2: {binary_result_2}")print(f"as int 2: {int(binary_result_2, base=2)}")
input 1: 42
output 1: 01000101
as int 1: 69
input 2: 69
output 2: 00101010
as int 2: 42
There is a Control Swap Gate as well which will be vital for correctly using this. So with everything set up can we create a probabilistic int flipper?
2-way Shuffle
It’s easy to put a qubit into the 50/50 superposition, as the Hadamard gate does that for us directly. If we use a qubit that has been put into that superposition as a control qubit then we can conditionally swap. Then roughly half of the time the two numbers will be swapped.
Code
from qiskit import QuantumCircuitvalue_1 =42value_2 =69binary_value_1 =bin(value_1)[2:][::-1]binary_value_2 =bin(value_2)[2:][::-1]qc = QuantumCircuit(17, 16) # one extra bit to determine if we swapfor index, value inenumerate(binary_value_1):if value =="1": qc.x(index)for index, value inenumerate(binary_value_2, start=8):if value =="1": qc.x(index)# the conditional qubitqc.h(qubit=16)for left_qubit, right_qubit inenumerate(range(8, 16)): qc.cswap(control_qubit=16, target_qubit1=left_qubit, target_qubit2=right_qubit)qc.measure(range(16), range(16))# the memory is arranged from high to low so the first 8 characters correspond to qubits 8-15# show_results(qc) ; Noneoutput = run(qc).get_counts()for key, count in output.items():print(f"{int(key[8:], base=2)} is the first value {count:,} times")
69 is the first value 509 times
42 is the first value 515 times
With this we have our first number shuffler!
3-way Shuffle
Extending this is tricky because there is no looping construct that I have found. The instructions that you are defining proceed forward in time, so every loop needs to be unrolled. I need to write this in a way where I don’t have to operate over previous “loop” results as that could get really messy.
With this in mind I want to be able to set one index at a time and then ignore it going forward. This leads to a simple algorithm, which was introduced at the start of this post:
For 0..N values
Pick one index and swap that with index 0
Consider the values 1..N and repeat
To make it easy to start with I am going to implement this manually as a 3 way shuffle. This will lead to large blocks of code. After that we can look at how to make it N-way.
I will start by defining a few functions that will be useful going forward:
And we can start by performing the first step, which will:
Swap nothing
OR Swap first number with second
OR Swap first number with third
Code
from qiskit import QuantumCircuitfrom random import randintimport mathcontrol_qubit_1 =8*3control_qubit_2 =8*3+1value_1 = randint(0, 255)value_2 = randint(0, 255)value_3 = randint(0, 255)qc = QuantumCircuit((8*3) +2, 8*3)initialize(qc, value=value_1, offset=0)initialize(qc, value=value_2, offset=8)initialize(qc, value=value_3, offset=8*2)# the conditional qubitsqc.rx(theta=to_rotation(1/3), qubit=control_qubit_1)qc.h(qubit=control_qubit_2)# turn off conditional qubit 2 if conditional qubit 1 is setqc.ch(control_qubit=control_qubit_1, target_qubit=control_qubit_2)conditional_swap(qc, control_qubit=control_qubit_1, left_offset=0, right_offset=8)conditional_swap(qc, control_qubit=control_qubit_2, left_offset=0, right_offset=8*2)qc.measure(range(8*3), range(8*3))output = run(qc).get_counts()print([value_1, value_2, value_3])for key, count in output.items():# remember that the high qubit values are in the _low_ string indexes,# so we step through this backwards values = [int(key[index-8:index], base=2)for index inrange(8*3, 0, -8) ]print(f"{values}: {count:,}")
To complete this 3 way shuffler the second and third numbers need to be conditionally swapped.
Code
from qiskit import QuantumCircuitfrom random import randintfrom itertools import permutationscontrol_qubit_1_1 =8*3control_qubit_1_2 =8*3+1control_qubit_2 =8*3+2# easier to see that it is working with these valuesvalue_1 =1value_2 =2value_3 =3qc = QuantumCircuit((8*3) +3, 8*3)initialize(qc, value=value_1, offset=0)initialize(qc, value=value_2, offset=8)initialize(qc, value=value_3, offset=8*2)# first swap: 0 with either 1 or 2 (or no swap)# the conditional qubitsqc.rx(theta=to_rotation(1/3), qubit=control_qubit_1_1)qc.h(qubit=control_qubit_1_2)# turn off conditional qubit 2 if conditional qubit 1 is setqc.ch(control_qubit=control_qubit_1_1, target_qubit=control_qubit_1_2)conditional_swap( qc, control_qubit=control_qubit_1_1, left_offset=0, right_offset=8)conditional_swap( qc, control_qubit=control_qubit_1_2, left_offset=0, right_offset=8*2)# second swap: 1 with 2 (or no swap)# the conditional qubitqc.h(qubit=control_qubit_2)conditional_swap( qc, control_qubit=control_qubit_2, left_offset=8, right_offset=8*2)qc.measure(range(8*3), range(8*3))# at this point, with barriers, the circuit image wraps 3 times# output = run(qc, shots=1_000_000).get_counts()output = run(qc).get_counts()print([value_1, value_2, value_3])for key in permutations([value_1, value_2, value_3]): index ="".join(f"{bin(value)[2:]:>08}"for value in key[::-1] )print(f"{key}: {output[index]:,}")
This can now produce all 6 permutations of a three element list.
One thing that is annoying is the fact that you can’t use a qubit as a control and as the target, which makes it hard to reliably clear a control qubit to reuse it later. For now I am going to use a new set of control qubits each time.
N-way Shuffle
Given we have a way to pick a value out of the list and then swap it with the first value, we can implement the shuffler by repeatedly invoking this, moving one value forward each time. This is nice because it has a fixed shape - the list elements to ignore are known irrespective of the outcome of the random process.
Code
from typing import Listfrom qiskit import QuantumCircuitimport mathdef nway_shuffle(values: List[int]) -> List[int]: qc = nway_shuffler(values) output = run(qc, shots=1).get_memory()[0]return [int(output[index*8:(index+1)*8], base=2)for index inrange(len(output)//8) ][::-1]def nway_shuffler(values: List[int], barriers: bool=False) -> QuantumCircuit: value_count =len(values)# one choice is skipped at each level control_qubits =sum(range(1, value_count)) qc = QuantumCircuit( (8* value_count) + control_qubits,8* value_count, )for index, value inenumerate(values): initialize(qc, value=value, offset=index*8)if barriers: qc.barrier() control_qubit_offset =len(values) *8for iteration inrange(value_count-1): count = value_count - iteration nway_shuffle_step( qc=qc, n=count, value_qubit_offset=8*iteration, control_qubit_offset=control_qubit_offset, ) control_qubit_offset += count-1if barriers: qc.barrier() qc.measure(range(8*value_count), range(8*value_count))return qcdef nway_shuffle_step( qc: QuantumCircuit, n: int, value_qubit_offset: int, control_qubit_offset: int,) ->None: nway_chooser( qc=qc, n=n, control_qubit_offset=control_qubit_offset )for index inrange(n-1): conditional_swap( qc, control_qubit=control_qubit_offset + index, left_offset=value_qubit_offset, right_offset=value_qubit_offset + (8*(index+1)) )def nway_chooser( qc: QuantumCircuit, n: int, control_qubit_offset: int,) ->None:# n less one because not doing something is a choice n = n -1 rotations = { v: to_rotation(1/(1+n-v))for v inrange(n) }for qubit, rotation in rotations.items(): qc.rx(theta=rotation, qubit=control_qubit_offset + qubit)for control_index inrange(n -1):for target_index inrange(control_index+1, n): control_qubit = control_qubit_offset + control_index target_qubit = control_qubit_offset + target_index target_rotation = rotations[target_index] qc.crx( theta=-target_rotation, control_qubit=control_qubit, target_qubit=target_qubit )
Code
nway_shuffle([1, 2, 3])
[3, 2, 1]
Running this once does not establish if it actually works though. We need to run it repeatedly to ensure that all of the possible values can be generated and that they are evenly distributed.
counts =list(results.get_counts().values())variance = (max(counts) /min(counts)) -1print(f"The most frequent result occurs {variance*100:0.3f}% more than the least frequent result")
The most frequent result occurs 0.833% more than the least frequent result
There are 6 different values and they are within ~1% of each other. I’m reasonably happy with this.
While this algorithm works it scales hilariously badly. My CPU / RAM weren’t even taxed that heavily. I think that this has become too large to effectively simulate?
Ultimately the qubit simulator maintains a matrix of every qubit and their interactions. It may well be that this matrix became too big to operate efficiently.
Optimizing for the Simulator
While I think that the implementation that I have is fine the underlying problem is that the system ends up in an extremely entangled state and there are a large number of qubits to simulate. I can address this by returning to the N-bit chooser from the previous post and gluing the other parts together in plain python. While it’s not ideal I do hope that this would be significantly faster.
The scaling here is approaching linear for the individual invocations. If we look at the cumulative time it has vastly improved compared to the full quantum implementation.
This is fast enough to be usable for non tiny lists.
These times are extremely similar to the times for the individual invocations which surprises me. Something to think about.
Conclusion
It’s a shame that I could not implement the full shuffler using quantum computing. I do think that this optimized algorithm is fast enough for my unstated goal…