Almost everyone starts learning quantum computing using single quantum systems (i.e. with just one qubit). However, soon enough we switch to multiple quantum systems (with two or more qubits) in our pursuit of knowledge about gates and algorithms. One of the first things we learn then are two-qubit gates, like the collection of controlled gates (ex. CNOT) or the SWAP gate.
This is usually the moment when you may get confused with Qiskit’s behaviour. You take a two-qubit gate like the SWAP gate, you apply it to |01⟩ for example, and in the results you see that nothing was swapped. In reality the gate worked as expected, and the confusion is rooted in the way Qiskit stores its state in memory.
Why is this happening? Let us start with implementing a program that can demonstrate this oddity.
Swapping Qubits’ States
There are many ways of changing the state of a two-qubit quantum system. I have already mentioned controlled gates which I believe are the most popular gates in quantum circuits nowadays. However, in this post I would like to use a simple example with a SWAP gate. This gate works in a very intuitive way, and is easy to understand even without robust quantum computing or computing in general knowledge.
The SWAP gate works in a pretty straightforward way: it swaps the values of two qubits. How does it exactly work? Every quantum gate can be represented with a unitary matrix. What a unitary matrix is is beyond the scope of this post. If you are nonetheless interested, I can tell for starters that it is a complex square matrix that, when multiplied by its conjugate transpose, results in the identity matrix. You can read more about unitary matrices here.
Without going into further details, here’s the matrix for the SWAP gate.
In order to apply it to a two qubits quantum system we have to multiply it by the matrix representing that system.
In short, two-qubit quantum systems are represented by a single column matrix that corresponds to every possible combination of those systems. You can see them all below.
(If you don’t know what these numbers are, please read my earlier blog post.)
When you apply this matrix to a ket, it simply swaps the second the the third coefficients. So in general:
(If you do not know what kets are, you can read about them here.)
The SWAP Gate in Action
In order to use the SWAP gate in our example, we must write a circuit that uses it. We start off with importing all necessary dependencies.
from itertools import product
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import Aer
Then, we define a set of variables that we’ll use later in the program.
register_qubits = 2
register_bits = 2
zero_ket = [1, 0]
one_ket = [0, 1]
# Generate all possible combinations of zero_ket and one_ket
ket_combinations = list(product([zero_ket, one_ket], repeat=2))
first_qubit = 0
second_qubit = 1
shots = 100
Their purpose will come clear when they’re used later in the code.
As I mentioned before, we can have four different combinations of two basic computational states: |0⟩ and |1⟩. Just as a reminder, they’re |00⟩, |01⟩, |10⟩ and |11⟩. In order to fully demonstrate what the SWAP gate does, we have to take a look at all of those combinations. We created them on line 9 of this program:
ket_combinations = list(product([zero_ket, one_ket], repeat=2))
This line essentially combines |0⟩ and |1⟩ into a collection of |00⟩, |01⟩, |10⟩ and |11⟩. We can then iterate over these combinations.
for ket_combination in ket_combinations:
The actual circuit is created and run within the loop. I will not go into details here, but please read what quantum circuits are and quantum simulators are if you would like to know more.
quantum_register = QuantumRegister(register_qubits, "q")
classical_register = ClassicalRegister(register_bits, "c")
circuit = QuantumCircuit(quantum_register, classical_register)
circuit.initialize(ket_combination[0], first_qubit)
circuit.initialize(ket_combination[1], second_qubit)
circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])
circuit.measure(quantum_register, classical_register)
simulator = Aer.get_backend("qasm_simulator")
transpiled_circuit = transpile(circuit, simulator)
result = simulator.run(transpiled_circuit, shots=shots).result()
print(f"For the first qubit {ket_combination[0]} and the second qubit {ket_combination[1]} the SWAP result is: {result.get_counts(circuit)}")
First off, we create a quantum and a classical register on lines 14 and 15. You can think of those registers as processor’s stack memory, the one that’s closest to it. The quantum register is used for storing qubits’ states, whereas the classical register is used for storing the values that are read out during measurement.
Then we implement the actual circuit (lines 16-20).
circuit = QuantumCircuit(quantum_register, classical_register)
circuit.initialize(ket_combination[0], first_qubit)
circuit.initialize(ket_combination[1], second_qubit)
circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])
circuit.measure(quantum_register, classical_register)
We define the circuit object by calling the QuantumCircuit(quantum_register, classical_register)
constructor.
We then initialise our two qubits with whatever kets we have in a given loop iteration.
circuit.initialize(ket_combination[0], first_qubit)
circuit.initialize(ket_combination[1], second_qubit)
Finally, we apply the SWAP gate…
circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])
…and measure.
circuit.measure(quantum_register, classical_register)
If you plotted this circuit with the following line of code – circuit.draw(output=’mpl’) – you’d get this diagram.
(I’ll cover circuit diagrams in one of the future blog posts.)
It represent the following quantum operation: SWAP|00⟩.
Run the program, and you’ll get the following results.
For the first qubit [1, 0] and the second qubit [1, 0] the SWAP result is: {'00': 100}
For the first qubit [1, 0] and the second qubit [0, 1] the SWAP result is: {'01': 100}
For the first qubit [0, 1] and the second qubit [1, 0] the SWAP result is: {'10': 100}
For the first qubit [0, 1] and the second qubit [0, 1] the SWAP result is: {'11': 100}
The number 100 is the number of times a given circuit was run (it’s important in a non-one probability readout like in this example).
[1, 0] is a row matrix that can be also written as |0⟩, and [0, 1] is a row matrix that can be written as |1⟩. So reading the results as they are yields this:
- |00⟩ → {’00’: 100}
- |01⟩ → {’01’: 100}
- |10⟩ → {’10’: 100}
- |11⟩ → {’11’: 100}
It looks like this program doesn’t work at all! But it does. The confusion lies in the way Qiskit stores values internally. It uses little endian.
Little Endian
What is little endian, or endianness in general? Endianness is the order in which bytes within a word of digital data are transmitted over a data communication medium or addressed (by rising addresses) in computer memory, counting only byte significance compared to earliness. The name is taken from the novel Gulliver’s Travels. You can read more about it here.
It sure sounds complicated, but in simpler words, endianness determines from which end of memory space the bytes are ordered. There are two types of endianness: the big endian and the little endian.
- A big-endian system stores the most significant byte of a word at the smallest memory address and the least significant byte at the largest.
- A little-endian system, in contrast, stores the least-significant byte at the smallest address.
Big endian is thus closer to the way the digits of numbers are written left-to-right in English, comparing digits to bytes. Little endian, on the other hand, reverses the natural ordering of the bits.
Here is a visual representation of these two systems.
Source: https://en.wikipedia.org/wiki/Endianness#/media/File:32bit-Endianess.svg
In the above example you can clearly see that while the big-endian preserves the natural ordering of a given processor word (or any binary value in general), the little-endian essentially reverses it. And this is the case with Qiskit. It uses internally little-endian to store quantum processing results, and hence prints them accordingly. For two-qubit systems it maps to the following list.
Qubits | Qiskit |
|00⟩ | 00 |
|01⟩ | 10 |
|10⟩ | 01 |
|11⟩ | 11 |
Getting the Big-endian Result
It is possible in Qiskit to change the ordering of the qubits from little-endian to big-endian. However, it is quite limited. For example, it can be easily done for circuit drawings with the reverse_bits()
function. Unfortunately, it looks like switching to big-endian for the Result object that we use to query for whatever we computed is not supported.
Conclusion
Interpreting results obtained from quantum circuits in Qiskit can be challenging. This is due to the fact that Qiskit uses little-endian to store computation results. It stores the least significant bit (the right-hand most) in the first memory index, and the rest of the bits follow. The result is hence a reverse of natural ordering that we expect in ket representations of quantum systems and can be confusing for new and experienced users.