Introduction to Quantum Error Correction

In [1]:
from qiskit import *

Correcting errors in phone calls

In this chapter, we will introduce the idea of quantum error correction. This will be done via the quantum repetition code: a simple quantum error correcting code that has all the basic features of more complex methods. Unfortunately, it is not a fully useful code in itself (which is why the more complex methods are needed). Nevertheless it is a great starting point, and a good first test of how well quantum error correction might work on a given device.

Before all this, it would be helpful to first introduce the idea of error correction in general.

Imagine you are speaking on the phone. Someone asks you a question to which the answer is 'yes' or 'no'. The way you give your response will depend on two factors:

  • How important is it that you are understood correctly?
  • How good is your connection?

Both of these can be paramaterized with probabilities. For the first, we can use $P_a$, the maximum acceptable probability of being misunderstood. If you are being asked to confirm a preference for ice cream flavours, and don't mind too much if you get vanilla rather than chocolate, $P_a$ might be quite high. If you are being asked a question on which someone's life depends, however, $P_a$ will be much lower.

For the second we can use $p$, the probability that your answer is garbled by a bad connectiom. For simplicity, let's imagine a case where a garbled 'yes' doesn't simply sound like nonsense, but sounds like a 'no'. And similarly a 'no' is transformed into 'yes'. Then $p$ is the probability that you are completely misunderstood.

If your connection is very good, or the answer is not very important, we will have $p<P_a$. The probability of being misunderstood in this case is so low that you don't care. The way you answer will then be simple: you just say 'yes' or 'no'.

If, however, your connection is poor and your answer is important, we will have $p>P_a$. A single 'yes' or 'no' is not enough in this case. The probability of being misunderstood would be too high.

So how do we overcome this problem? The answer is something you would probably do without thinking: repeat yourself. Instead of 'yes', say 'yes, yes, yes'. Instead of 'no', say 'no, no no'.

If the person you are talking to hears 'yes, yes, yes', they will of course conclude that you meant 'yes'. If they hear 'no, yes, yes', 'yes, no, yes' or 'yes, yes, no', they will probably conclude the same thing, since there is more positivity than negativity in the answer. To be misunderstood in this case, at least two of your replies need to be garbled. The probability for this, $P$, will be less than $p$. So your message will be more likely to be understood.

In [2]:
p = 0.01
P = 3 * p**2 * (1-p) + p**3
print('Probability of a single reply being garbled:',p)
print('Probability of a the majority of three replies being garbled:',P)
Probability of a single reply being garbled: 0.01
Probability of a the majority of three replies being garbled: 0.00029800000000000003

If $P<P_a$, this technique solves our problem. If not, we can simply add more repetitions. The fact that $P<p$ above comes from the fact that we need at least two replies to be garbled to flip the majority, and so even the most likely possibilities have a probability of $\sim p^2$. For five repetitions we'd need at least three replies to be garbled to flip the majority, which happens with probability $\sim p^3$. The value for $P$ in this case would then be even lower. Indeed, as we increase the number of repetitions, $P$ will decrease exponentially. No matter how bad the connection, or how certain we need to be of our message getting through correctly, we can acheive it by just repeating our answer enough times.

Though this is a simple example, it contains all the aspects of error correction.

  • There is some information to be sent or stored: In this case, a 'yes' or 'no.
  • The information is encoded in a larger system to protect it against noise: In this case, by repeating the message.
  • The information is finally decoded, mitigating for the effects of noise: In this case, by trusting the majority of the transmitted messages.

It is using these steps that we correct errors in quantum computers. But first, let's implement this simple repetition encoding using qubits.

Correcting errors in qubits

Let's just try to store a simple 0 or a 1 in some qubits. Of course, we know that noise exists in our system. This can have the effect of causing us to readout a 1 for a qubit that should be 0, and vice versa.

Here is some Qiskit code that provides a simple example of such noise. In this we go beyond the simple case of a single noise event which happens with a probability $p$. Instead we consider two forms of error that can occur. One is a gate error: an imperfection in any operation we perform. We model this here in a simple way, using so-called depolarizing noise. The effect of this will be, with probabilty $p_{gate}$ ,to replace the state of any qubit with a completely random state. For two qubit gates, it is applied independently to each qubit.

The other form of noise is that for measurement. This simply flips between a 0 to a 1 and vice-versa immediately before measurement with probability $p_{meas}$.

In [3]:
from qiskit.providers.aer.noise import NoiseModel
from qiskit.providers.aer.noise.errors import pauli_error, depolarizing_error

def get_noise(p_meas,p_gate):

    error_meas = pauli_error([('X',p_meas), ('I', 1 - p_meas)])
    error_gate1 = depolarizing_error(p_gate, 1)
    error_gate2 = error_gate1.tensor(error_gate1)

    noise_model = NoiseModel()
    noise_model.add_all_qubit_quantum_error(error_meas, "measure") # measurement error is applied to measurements
    noise_model.add_all_qubit_quantum_error(error_gate1, ["x"]) # single qubit gate error is applied to x gates
    noise_model.add_all_qubit_quantum_error(error_gate2, ["cx"]) # two qubit gate error is applied to cx gates
        
    return noise_model

As an example, we'll now create such a noise model with a probability of $1\%$ for each type of error.

In [4]:
noise_model = get_noise(0.01,0.01)

Let's see what affect this has when try to store a '0' using three qubits in state $\left|0\right\rangle$. We'll repeat the process shots=10000 times to see how likely different results are.

In [5]:
qc0 = QuantumCircuit(3,3,name='0') # initialize circuit with three qubits in the 0 state

qc0.measure(qc0.qregs[0],qc0.cregs[0]) # measure the qubits

# run the circuit with th noise model and extract the counts
counts = execute( qc0, Aer.get_backend('qasm_simulator'),noise_model=noise_model,shots=10000).result().get_counts()

print(counts)
{'010': 70, '100': 100, '011': 1, '000': 9731, '001': 98}

As you can see, almost all results still come out '000', as they would if there was no noise. Of the remaining possibilities, those with a majority of 0s are most likely. In total, much less than 100 samples come out with a majority of 1s, so $P<1\%$.

Now let's try the same for storing a '1' using three qubits in state $\left|1\right\rangle$.

In [6]:
qc1 = QuantumCircuit(3,3,name='0') # initialize circuit with three qubits in the 0 state
qc1.x(qc1.qregs[0]) # flip each 0 to 1

qc1.measure(qc1.qregs[0],qc1.cregs[0]) # measure the qubits

# run the circuit with th noise model and extract the counts
counts = execute( qc1, Aer.get_backend('qasm_simulator'),noise_model=noise_model,shots=10000).result().get_counts()

print(counts)
{'010': 3, '100': 5, '101': 168, '110': 150, '111': 9530, '011': 140, '001': 4}

The number of samples that come out with a majority in the wrong state (0 in this case) is again much less than 100, so $P<1\%$. Whether we store a 0 or a 1, we can retrieve the information with a smaller probability of error than either of our sources of noise.

This was possible because the noise we considered was relatively weak. As we increase $p_{meas}$ and $p_{gate}$, the higher the probability $P$ will be. The extreme case of this is for either of them to have a $50/50$ chance of applying the bit flip error, x. For example, let's run the same circuit as before but with $p_{meas}=0.5$ and $p_{gate}=0$.

In [7]:
noise_model = get_noise(0.5,0.0)
counts = execute( qc1, Aer.get_backend('qasm_simulator'),noise_model=noise_model,shots=10000).result().get_counts()
print(counts)
{'010': 1215, '100': 1278, '101': 1304, '111': 1173, '011': 1255, '001': 1258, '110': 1310, '000': 1207}

With this noise, all outcomes occur with equal probability. No trace of the encoded state remains. This is an important point to consider for error correction: sometimes the noise is too strong to be corrected. The optimal approach is to combine a good way of encoding the information you require, with hardware that whose noise is not too strong.

Storing qubits

So far, we have considered cases where there is no delay between encoding and decoding. For qubits, this means that there is no significant amount of time that passes between initializing the circuit, and making the final measurements.

However, there are many cases for which there will be a significant delay. As an obvious example, one may wish to encode a quantum state and store it for a long time, like a quantum hard drive. A less obvious but much more important example is performing fault-tolerant quantum computation itself. For this, we need to store quantum states and preserve their integrity during the computation. This must also be done in a way that allows us to manipulate the stored information in any way we need, and which corrects any errors we may introduce when performing the manipulations.

In all cases, we need account for the fact that errors do not only occur when something happens (like a gate or measurement), they also occur when the qubits are idle. Such noise is due to the fact that the qubits interact with each other and their environment. The longer we leave our qubits idle for, the greater the effects of this noise becomes. If we leave them for long enough, we'll encounter a situation like the $p_{meas}=0.5$ case above, where the noise is too strong for errors to be reliably corrected.

The solution is to keep measuring throughout. Don't leave your qubit idle for too long, but keep extracting information to keep track of the errors that have occurred.

For the case of classical information, where we simply wish to store a 0 or 1, this can be done by just constantly measuring the value of each qubit. Then we will keep track of when the values change due to noise, and can easily deduce a history of when errors occurred.

For quantum information, however, it is not so easy. For example, consider the case that we wish to encode the state $\left|+\right\rangle$. Our encoding is such that

$$\left|0\right\rangle \rightarrow \left|000\right\rangle,~~~ \left|1\right\rangle \rightarrow \left|111\right\rangle.$$

To encode the $\left|+\right\rangle$ state we therefore need

$$\left|+\right\rangle=\frac{1}{\sqrt{2}}\left(\left|0\right\rangle+\left|1\right\rangle\right)\rightarrow \frac{1}{\sqrt{2}}\left(\left|000\right\rangle+\left|111\right\rangle\right).$$

It will be convenient to now introduce some terminology. In the expression above, there are two different concepts that we can refer to as a 'qubit'. One is that of the 'physical qubit'. These are the actual physical qubits that exist on a chip. There are three physical qubits in the above expression. The other type of qubit is the 'logical' qubit. This is a qubit state, encoded across multiple physical qubits, with the hope that our physical qubit will experience less noise than the physical qubits they are made up of. In the above expression there is a single logical qubit, which is in the state $\left|+\right\rangle$ and encoded across three physical qubits.

With the repetition encoding that we are using, a z measurement of the logical qubit is done using a z measurement of each physical qubit. The final result for the logical measurement is decoded from the physical qubit measurement results by simply looking which output is in the majority.

As mentioned earlier, we can keep track of errors on logical qubits that are stored for a long time by constantly performing z measurements of the physical qubits. However, note that this effectively corresponds to constantly peforming z measurements of the physical qubits. This is fine if we are simply storing a 0 or 1, but it has undesired effects if we are storing a superposition. Specifically: the first time we do such a check for errors, we will collapse the superposition!

This is not ideal. If we wanted to do some computation on our logical qubit, or is we wish to peform a basis change before final measurement, we need to preserve the superposition. Destroying it is an error. But this is not an error caused by imperfections in our devices. It is an error that we have introduced as part of our attempts to correct errors. And since we cannot hope to recreate any arbitrary superposition stored in our quantum computer, it is an error than cannot be corrected.

For this reason, we must find another way of keeping track of the errors that occur when our logical qubit is stored for long times. This should give us the information we need to detect and correct errors, and to decode the final measurment result with high probability. However, it should not cause uncorrectable errors to occur during the process by collapsing superpositions that we need to preserve.

The way to do this is with the following circuit element.

In [8]:
from qiskit import *
cq = QuantumRegister(2,'code_qubit')
lq = QuantumRegister(1,'ancilla_qubit')
sb = ClassicalRegister(1,'syndrome_bit')
qc = QuantumCircuit(cq,lq,sb)
qc.cx(cq[0],lq[0])
qc.cx(cq[1],lq[0])
qc.measure(lq,sb)
print(qc)
                                 
   code_qubit_0: |0>──■──────────
                      │          
   code_qubit_1: |0>──┼────■─────
                    ┌─┴─┐┌─┴─┐┌─┐
ancilla_qubit_0: |0>┤ X ├┤ X ├┤M├
                    └───┘└───┘└╥┘
  syndrome_bit_0: 0 ═══════════╩═
                                 

Here we have three physical qubits. Two are called 'code qubits', and the other is called an 'ancilla qubit'. One bit of output is extracted, called the syndrome bit.

The ancilla qubit will always be initialized in state $\left|0\right\rangle$. The code qubits, however, can be initialized in different states. Let's see what output we get for different initial states of the code qubits. We'll do this by creating a circuit qc_init that prepares the code qubits in some state, and then run the circuit qc_init+qc.

First, the trivial case: qc_init does nothing, and so the code qubits are initially $\left|00\right\rangle$.

In [9]:
qc_init = QuantumCircuit(cq)

print(qc_init+qc)

counts = execute( qc_init+qc, Aer.get_backend('qasm_simulator'),shots=10000).result().get_counts()
print('\nResults:',counts)
                                 
   code_qubit_0: |0>──■──────────
                      │          
   code_qubit_1: |0>──┼────■─────
                    ┌─┴─┐┌─┴─┐┌─┐
ancilla_qubit_0: |0>┤ X ├┤ X ├┤M├
                    └───┘└───┘└╥┘
  syndrome_bit_0: 0 ═══════════╩═
                                 

Results: {'0': 10000}

The outcome, in all cases, is '0'.

Now let's try an initial state of $\left|11\right\rangle$.

In [10]:
qc_init = QuantumCircuit(cq)
qc_init.x(cq)

print(qc_init+qc)

counts = execute( qc_init+qc, Aer.get_backend('qasm_simulator'),shots=10000).result().get_counts()
print('\nResults:',counts)
                    ┌───┐             
   code_qubit_0: |0>┤ X ├──■──────────
                    ├───┤  │          
   code_qubit_1: |0>┤ X ├──┼────■─────
                    └───┘┌─┴─┐┌─┴─┐┌─┐
ancilla_qubit_0: |0>─────┤ X ├┤ X ├┤M├
                         └───┘└───┘└╥┘
  syndrome_bit_0: 0 ════════════════╩═
                                      

Results: {'0': 10000}

The outcome is also always '0'.

Now let's try a superposition of $\left|00\right\rangle$ and $\left|11\right\rangle$.

In [11]:
qc_init = QuantumCircuit(cq)
qc_init.h(cq[0])
qc_init.cx(cq[0],cq[1])


print(qc_init+qc)

counts = execute( qc_init+qc, Aer.get_backend('qasm_simulator'),shots=10000).result().get_counts()
print('\nResults:',counts)
                    ┌───┐                  
   code_qubit_0: |0>┤ H ├──■────■──────────
                    └───┘┌─┴─┐  │          
   code_qubit_1: |0>─────┤ X ├──┼────■─────
                         └───┘┌─┴─┐┌─┴─┐┌─┐
ancilla_qubit_0: |0>──────────┤ X ├┤ X ├┤M├
                              └───┘└───┘└╥┘
  syndrome_bit_0: 0 ═════════════════════╩═
                                           

Results: {'0': 10000}

Again, the outcome is always '0'.

Now let's try a superposition of $\left|01\right\rangle$ and $\left|10\right\rangle$ instead.

In [12]:
qc_init = QuantumCircuit(cq)
qc_init.h(cq[0])
qc_init.cx(cq[0],cq[1])
qc_init.x(cq[0])


print(qc_init+qc)

counts = execute( qc_init+qc, Aer.get_backend('qasm_simulator'),shots=10000).result().get_counts()
print('\nResults:',counts)
                    ┌───┐     ┌───┐             
   code_qubit_0: |0>┤ H ├──■──┤ X ├──■──────────
                    └───┘┌─┴─┐└───┘  │          
   code_qubit_1: |0>─────┤ X ├───────┼────■─────
                         └───┘     ┌─┴─┐┌─┴─┐┌─┐
ancilla_qubit_0: |0>───────────────┤ X ├┤ X ├┤M├
                                   └───┘└───┘└╥┘
  syndrome_bit_0: 0 ══════════════════════════╩═
                                                

Results: {'1': 10000}

For this the output is always '1'. If you try the input $\left|01\right\rangle$ or $\left|10\right\rangle$ you will find the same.

This measurement is therefore telling us about a collective property of multiple qubits. Specifically, it looks at the two code qubits and determines whether their state is the same or different in the z basis. For basis states that are the same in the z basis, like $\left|00\right\rangle$ and $\left|11\right\rangle$, the measurement simply returns '0'. It also does so for any superposition of these. Since it does not distinguish between these states in any way, it also does not collapse such a superposition.

Similarly, For basis states that are different in the z basis it returns a '1'. This occurs for $\left|01\right\rangle$, $\left|10\right\rangle$ or any superposition thereof.

Now suppose we apply such a 'syndrome measurement' on all pairs of physical qubits in our repetition code. If their state is described by a repeated $\left|0\right\rangle$, a repeated $\left|1\right\rangle$, or any superposition thereof, all the syndrome measurements will return '0'. Given this result, we will know that our states are indeed encoded in the repeated states that we want them to be, and can deduce that no errors have occurred. If some syndrome measurements return '1', however, it is a signature of an error. We can therefore use these measurement results to determine how to decode the result.

Quantum repetition code

We now know enough to understand exactly how the quantum version of the repetition code is implemented

We can use it in Qiskit by importing the required tools from Ignis.

In [13]:
from qiskit.ignis.verification.topological_codes import RepetitionCode
from qiskit.ignis.verification.topological_codes import lookuptable_decoding

We are free to choose how many physical qubits we want the logical qubit to be encoded in. So far we have focussed on three repetitions, so let's continue with that.

In [14]:
n = 3

We can also choose how many times the syndrome measurements will be applied while we store our logical qubit, before the final readout measurement. Let's just go for one round.

In [15]:
T = 1

The circuits for our code can then be created automatically from the using a RepetitionCode object from Ignis.

In [16]:
code = RepetitionCode(n,T)

With this we can inspect various properties of the code, such as the names of the qubit registers used for the code and ancilla qubits.

In [17]:
code.qubit_registers
Out[17]:
{'code_qubit', 'link_qubit'}

These registers are also attributes of the repetition_code object.

In [18]:
code.code_qubit
Out[18]:
QuantumRegister(3, 'code_qubit')

The RepetitionCode contains two quantum circuits that implement the code: One for each of the two possible logical bit values.

In [19]:
for log in ['0','1']:
    print('\n========= logical',log,'=========\n')
    print( code.circuit[log] )
========= logical 0 =========

                      ┌───┐     ┌───┐     ┌─┐         ░          
     link_qubit_0: |0>┤ X ├─────┤ X ├─────┤M├────|0>──░──────────
                      └─┬─┘┌───┐└─┬─┘┌───┐└╥┘┌─┐      ░          
     link_qubit_1: |0>──┼──┤ X ├──┼──┤ X ├─╫─┤M├─|0>──░──────────
                        │  └─┬─┘  │  └─┬─┘ ║ └╥┘      ░ ┌─┐      
     code_qubit_0: |0>──■────┼────┼────┼───╫──╫───────░─┤M├──────
                             │    │    │   ║  ║       ░ └╥┘┌─┐   
     code_qubit_1: |0>───────■────■────┼───╫──╫───────░──╫─┤M├───
                                       │   ║  ║       ░  ║ └╥┘┌─┐
     code_qubit_2: |0>─────────────────■───╫──╫───────░──╫──╫─┤M├
                                           ║  ║       ░  ║  ║ └╥┘
round_0_link_bit_0: 0 ═════════════════════╩══╬══════════╬══╬══╬═
                                              ║          ║  ║  ║ 
round_0_link_bit_1: 0 ════════════════════════╩══════════╬══╬══╬═
                                                         ║  ║  ║ 
        code_bit_0: 0 ═══════════════════════════════════╩══╬══╬═
                                                            ║  ║ 
        code_bit_1: 0 ══════════════════════════════════════╩══╬═
                                                               ║ 
        code_bit_2: 0 ═════════════════════════════════════════╩═
                                                                 

========= logical 1 =========

                            ░ ┌───┐     ┌───┐     ┌─┐         ░          
     link_qubit_0: |0>──────░─┤ X ├─────┤ X ├─────┤M├────|0>──░──────────
                            ░ └─┬─┘┌───┐└─┬─┘┌───┐└╥┘┌─┐      ░          
     link_qubit_1: |0>──────░───┼──┤ X ├──┼──┤ X ├─╫─┤M├─|0>──░──────────
                      ┌───┐ ░   │  └─┬─┘  │  └─┬─┘ ║ └╥┘      ░ ┌─┐      
     code_qubit_0: |0>┤ X ├─░───■────┼────┼────┼───╫──╫───────░─┤M├──────
                      ├───┤ ░        │    │    │   ║  ║       ░ └╥┘┌─┐   
     code_qubit_1: |0>┤ X ├─░────────■────■────┼───╫──╫───────░──╫─┤M├───
                      ├───┤ ░                  │   ║  ║       ░  ║ └╥┘┌─┐
     code_qubit_2: |0>┤ X ├─░──────────────────■───╫──╫───────░──╫──╫─┤M├
                      └───┘ ░                      ║  ║       ░  ║  ║ └╥┘
round_0_link_bit_0: 0 ═════════════════════════════╩══╬══════════╬══╬══╬═
                                                      ║          ║  ║  ║ 
round_0_link_bit_1: 0 ════════════════════════════════╩══════════╬══╬══╬═
                                                                 ║  ║  ║ 
        code_bit_0: 0 ═══════════════════════════════════════════╩══╬══╬═
                                                                    ║  ║ 
        code_bit_1: 0 ══════════════════════════════════════════════╩══╬═
                                                                       ║ 
        code_bit_2: 0 ═════════════════════════════════════════════════╩═
                                                                         

In these circuits, we have two types of physical qubits. There are the 'code qubits', which are the three physical qubits across which the logical state is encoded. There are also the 'link qubits', which serve as the ancilla qubits for the syndrome measurements.

Our single round of syndrome measurements in these circuits consist of just two syndrome measurements. One compares code qubits 0 and 1, and the other compares code qubits 1 and 2. One might expect that a further measurement, comparing code qubits 0 and 2, should be required to create a full set. However, these two are sufficient. This is because the information on whether 0 and 2 have the same z basis state can be inferred from the same information about 0 and 1 with that for 1 and 2. Indeed, for $n$ qubits, we can get the required information from just $n-1$ syndrome measurements of neighbouring pairs of qubits.

Let's now run these circuits without noise and see what happens.

In [20]:
circuits = code.get_circuit_list()
job = execute( circuits, Aer.get_backend('qasm_simulator') )
for log in ['0','1']:
    print('\nLogical',log,':',job.result().get_counts(log))
Logical 0 : {'000 00': 1024}

Logical 1 : {'111 00': 1024}

Here we see that the output comes in two parts. The part on the right holds the outcomes of the two syndrome measurements. That on the left holds the outcomes of the three final measurements of the code qubits.

For more measurement rounds, $T=4$ for example, we would have the results of more syndrome measurements on the right.

In [21]:
code = RepetitionCode(n,4)

circuits = code.get_circuit_list()
job = execute( circuits, Aer.get_backend('qasm_simulator') )
for log in ['0','1']:
    print('\nLogical',log,':',job.result().get_counts(log))
Logical 0 : {'000 00 00 00 00': 1024}

Logical 1 : {'111 00 00 00 00': 1024}

For more repetition, $n=5$ for example, each set of measurements would be larger. The final measurement on the left would be of $n$ qubits. The $T$ syndrome measurements would each be of the $n-1$ possible neighbouring pairs.

In [22]:
code = RepetitionCode(5,4)

circuits = code.get_circuit_list()
job = execute( circuits, Aer.get_backend('qasm_simulator') )
for log in ['0','1']:
    print('\nLogical',log,':',job.result().get_counts(log))
Logical 0 : {'00000 0000 0000 0000 0000': 1024}

Logical 1 : {'11111 0000 0000 0000 0000': 1024}
In [23]:
code = RepetitionCode(n,4)

circuits = code.get_circuit_list()
job = execute( circuits, Aer.get_backend('qasm_simulator') )
for log in ['0','1']:
    print('\nLogical',log,':',job.result().get_counts(log))
Logical 0 : {'000 00 00 00 00': 1024}

Logical 1 : {'111 00 00 00 00': 1024}

Now let's return to the $n=3$, $T=1$ example and look at a case with some noise.

In [24]:
code = RepetitionCode(3,1)

noise_model = get_noise(0.2,0.2)

circuits = code.get_circuit_list()
job = execute( circuits, Aer.get_backend('qasm_simulator'), noise_model=noise_model )
raw_results = {}
for log in ['0','1']:
    raw_results[log] = job.result().get_counts(log)
    print('\n========= logical',log,'=========\n')
    print(raw_results[log])
========= logical 0 =========

{'000 10': 80, '001 00': 68, '010 01': 45, '110 01': 14, '111 00': 13, '111 11': 2, '010 00': 88, '011 10': 8, '101 10': 9, '010 11': 18, '001 11': 11, '011 00': 28, '111 10': 4, '011 11': 8, '100 00': 56, '011 01': 16, '100 11': 11, '101 11': 1, '100 10': 16, '000 00': 174, '010 10': 33, '111 01': 7, '110 10': 14, '110 11': 11, '100 01': 30, '110 00': 22, '001 01': 30, '000 11': 36, '000 01': 102, '001 10': 35, '101 01': 11, '101 00': 23}

========= logical 1 =========

{'000 10': 7, '001 00': 19, '010 01': 14, '110 01': 40, '111 00': 145, '111 11': 38, '010 00': 25, '011 10': 57, '101 10': 32, '010 11': 9, '001 11': 11, '011 00': 50, '111 10': 75, '011 11': 24, '100 00': 21, '011 01': 30, '100 11': 12, '101 11': 37, '100 10': 14, '000 00': 11, '010 10': 13, '111 01': 73, '110 10': 29, '110 11': 15, '100 01': 23, '110 00': 51, '001 01': 15, '000 11': 5, '000 01': 11, '001 10': 21, '101 01': 48, '101 00': 49}

Here we have created raw_results, a dictionary that holds both the results for a circuit encoding a logical 0 and 1 encoded for a logical 1.

Our task when confronted with any of the possible outcomes we see here is to determine what the outcome should have been, if there was no noise. For an outcome of '000 00' or '111 00', the answer is obvious. These are the results we just saw for a logical 0 and logical 1, respectively, when no errors occur. The former is the most common outcome for the logical 0 even with noise, and the latter is the most common for the logical 1. We will therefore conclude that the outcome was indeed that for logical 0 whenever we encounter '000 00', and the same for logical 1 when we encounter '111 00'.

Though this tactic is optimal, it can nevertheless fail. Note that '111 00' typically occurs in a handful of cases for an encoded 0, and '00 00' similarly occurs for an encoded 1. In this case, through no fault of our own, we will incorrectly decode the output. In these cases, a large number of errors conspired to make it look like we had a noiseless case of the opposite logical value, and so correction becomes impossible.

We can employ a similar tactic to decode all other outcomes. The outcome '001 00', for example, occurs far more for a logical 0 than a logical 1. This is because it could be caused by just a single measurement error in the former case (which incorrectly reports a single 0 to be 1), but would require at least two errors in the latter. So whenever we see '001 00', we can decode it as a logical 0.

Applying this tactic over all the strings is a form of so-called 'lookup table decoding'. This is where every possible outcome is analyzed, and the most likely value to decode it as is determined. For many qubits, this quickly becomes intractable, as the number of possible outcomes becomes so large. In these cases, more algorithmic decoders are needed. However, lookup table decoding works well for testing out small codes.

We can use tools in Qiskit to implement lookup table decoding for any code. For this we need two sets of results. One is the set of results that we actually want to decode, and for which we want to calcate the probability of incorrect decoding, $P$. We will use the raw_results we already have for this.

The other set of results is one to be used as the lookup table. This will need to be run for a large number of samples, to ensure that it gets good statistics for each possible oiutcome. We'll use shots=10000.

In [25]:
job = execute( circuits, Aer.get_backend('qasm_simulator'), noise_model=noise_model, shots=10000 )
table_results = {}
for log in ['0','1']:
    table_results[log] = job.result().get_counts(log)

With this data, which we call table_results, we can now use the lookuptable_decoding function from Qiskit. This takes each outcome from raw_results and decodes it with the information in table_results. Then it checks if the decoding was correct, and uses this information to calculate $P$.

In [26]:
P = lookuptable_decoding(raw_results,table_results)
print('P =',P)
P = {'0': 0.201, '1': 0.2073}

Here we find $P$ to be similar to $p_{meas}$ and $p_{gate}$. The value of $P$ for an encoded 1 is higher than that for 0. This is because the encoding of 1 requires the application of x gates, which are an additional source of noise.

As always with quantum error correction, more impressive results can be found when the noise is not so strong. For example, $p_{meas}=p_{gate}=0.1$

In [27]:
code = RepetitionCode(3,1)

noise_model = get_noise(0.1,0.1)

circuits = code.get_circuit_list()

job = execute( circuits, Aer.get_backend('qasm_simulator'), noise_model=noise_model )
raw_results = {}
for log in ['0','1']:
    raw_results[log] = job.result().get_counts(log)

job = execute( circuits, Aer.get_backend('qasm_simulator'), noise_model=noise_model, shots=10000 )
table_results = {}
for log in ['0','1']:
    table_results[log] = job.result().get_counts(log)
    
P = lookuptable_decoding(raw_results,table_results)
print('P =',P)
P = {'0': 0.0678, '1': 0.0733}

Here $P$ is less than either $p_{meas}$ or $p_{gate}$, and much less than the two combined. We therefore have a considerable improvement. If you try it with larger values of $n$ and you'll see even better results. Though note that the value of shots used may need to be increased also.

Basic principles of quantum error correction

As mentioned that the start of this section, the repetition code is a simple example of the basic principles of quantum error correction. These are as follows.

  1. The information we wish to store and process takes the form of 'logical qubits'. The states of these are encoded across many of the actual 'physical qubits' of a device.

  2. Information about errors is extracted constantly through a process of 'syndrome' measurement. These consist of measurements that extract no information about the logical stored information. Instead they assess collective properties of groups of physical qubits, in order to determine when faults arise in the encoding of the logical qubits.

  3. The information from syndrome measurements allows the effects of errors to be identified and mitigated for with high probability. This requires a decoding method.

There is another basic principle for which the repetition code is not such a good example.

  1. Manipulating stored information must require action on multiple physical qubits. The minimum number required for any code is is known as the distance of the code, $d$. Possible manipulations include performing an x operation on the logical qubit (flipping an encoded $\left|0\right\rangle$ to an encoded $\left|1\right\rangle$, and vice-versa), or performing a logical z measurement (distinguishing an encoded $\left|0\right\rangle$ from an encoded $\left|1\right\rangle$).

This makes it harder to perform operations on logical qubits when required: both for us, and for errors. The latter is, of course, the reason why this behaviour is required. If logical information could be acessed using only a single physical qubit, it would always be possible for single stray errors to disturb the logical qubit. The aim is usually to make it relatively straightforward for us to perform logical operations, given that we know how to do it, but hard for noise to achieve it by random chance.

In terms of making it hard for noise to peform a logical x, the repetition code cannot be beaten: All code qubits must be flipped to flip the logical value. From this perspective, $d=n$. For a z measurement, however, the repetition code is very poor. In the ideal case of no errors, the logical z basis information is repeated across every code qubit. Measuring any single code qubit is therefore sufficient to deduce the logical value. For this logical operation, and the overal distance, is therefore $d=1$ for the repetition code. This is also reflected by the fact that the code is unable to detect and correct logical z errors.

For a better example of quantum error correction, we therefore need to find alternatives to the repetition approach. One of the foremost examples is the surface code, which will be added to this textbook as soon as it is implemented in Ignis.

In [ ]: