IBM Quantum Challenge: Spring 2023 — Lab 4

Exploring the World of Quantum Computing: IBM’s Quantum Challenge Spring 2023 -Quantum Error Correction

Monit Sharma
17 min readMay 31, 2023

Introduction:

Welcome back to our blog series on the recently concluded IBM Quantum Challenge Spring 2023! In this series, we are delving into the exciting world of quantum computing and exploring the various lab exercises that participants tackled during the challenge. In this fourth article, we will be focusing on Lab 4, which centers around the critical concept of Quantum Error Correction. We will delve into the fundamentals of error correction, explore its application to classical data, and then demonstrate its implementation on quantum computers. Additionally, we will discuss the challenges of running error correction on real quantum devices.

Understanding Error Correction

Error correction is a vital technique in both classical and quantum computing, aimed at mitigating the adverse effects of errors and noise. In classical computing, error correction mechanisms ensure the integrity and accuracy of data transmission and storage. In the realm of quantum computing, error correction becomes even more crucial due to the inherent fragility of quantum states and susceptibility to decoherence and other forms of noise.

The basics of error correction. ‘Measurement’ qubits can detect errors on ‘data’ qubits through the use of quantum XOR gates. (img from : A step closer to quantum computation with Quantum Error Correction)

Applying Error Correction to Classical Data

Before diving into quantum error correction, it is essential to understand how error correction is employed in classical computing. This understanding will provide a solid foundation for grasping the concepts and techniques employed in quantum error correction.

Quantum Error Correction

Introduction to Bit Flip Codes, In this lab exercise, participants were introduced to the fascinating world of quantum error correction. The focus was on bit flip codes, which are a type of quantum error correction code used to protect quantum states against bit flip errors. Participants learned how to implement bit flip codes by encoding quantum information into larger code states, applying error-detecting measurements, and correcting the errors through appropriate recovery operations.

Challenges of Running Error Correction on Real Quantum Devices

While error correction is a powerful tool in theory, implementing it on real quantum devices poses significant challenges. The lab delved into the practical considerations involved in running error correction on quantum hardware, such as the limited coherence times, noise sources, and imperfect gates. Participants explored strategies to mitigate these challenges and gained insights into the trade-offs between error correction overhead and error mitigation.

Error Correction

We saw a lot of use case for Dynamic circuits in the previous lab exercises, it was helpful as a subroutine for the Phase estimation algorithm and can help us with the mid-circuit measurements. Naturally, they also have the potential to help us with error correction on quantum computing.

Error correction is very important in quantum computing, since the real hardware (qubits) are susceptible to noise. Which means that there’s always a small probability that some error will occur while we are running our circuit. It can be as small as bit-flip, where a qubit is flipped from 0 to 1. Error correction helps us in dealing with these small probabilities and ensure that we get the correct result in the end, by correcting the errors as they occur.

You can read about Quantum Error Correction on Qiskit Textbook.

Let’s import some basic stuff needed for the lab:

# Importing all the parts we will need.
from typing import List, Optional

from qiskit import transpile, QuantumCircuit, QuantumRegister, ClassicalRegister, Aer
from qiskit.providers.fake_provider import FakeManilaV2
from qiskit.visualization import plot_histogram


import warnings

warnings.filterwarnings("ignore")

import math

pi = math.pi

Classical Error Correction

Why even is there a need for error correction?

Imagine trying to talk to someone on the phone when the signal is weak. Even if you try really hard to speak clearly, sometimes the message gets messed up. For example, you might say, “Let’s plan to have lunch with Fred on Friday,” but the person on the other end hears, “Let’s hit Fred on Friday.” Poor Fred’s weekend just got a lot worse.

Whenever we even try to send a message as trivial as a single bit, there’s still a probability of some small error to happen. If you send one bit of data over a wire, there may be a small chance that the bit is flipped, so instead of a 0 the recipient gets a 1. This can happen more often if the cable is old and corroded. The same could potentially happen when storing data -- think of a CD getting scratched. In the worst case, errors could even happen during a computation in your processor. It is a well researched problem in classical information.

Let’s imagine that the chance of a bit accidentally being flipped (AKA error to occur) is 10%. This would mean that for each bit of data we send, there is a one-in-ten chance that the bit received is incorrect. You could send a message of 0 ten times, but the received message might read 0001000000. You can see the problem already.

One of the simplest solutions is using some repetition. My bit of data 1 can get encoded as 111, and likewise a 0 gets encoded as 000. Each bit of data is now encoded using three bits instead of just one.

Why does this help?

If we now send 000 and error occurs, then the receiver might see 001. Since the receiver knows that the should have gotten either 000 or 111, they could deduce that it was probably 000 that was sent, and a single bit got flipped. Errors, corrected!

But what if multiple errors occur? Our message gets turned into 011 and the recipient now assumes we meant to send 111. True error! By using the repetition, we reduce the chance that that happens, but we don't eliminate it. So how much do we reduce the error rate by, exactly? Let's investigate!

You can find the probability yourself on how the errors change with the code below:

# Probability for a 000 message to contain one error:
p1 = 0.10

# Calculating the probability of a message containing 2 or 3 errors:
p3 = 3 * p1 * p1 * (1 - p1) + p1 * p1 * p1

print("Percent chance the 000 message has one error: {}".format(p1 * 100))
print("Percent chance the 000 message has 2 or 3 errors: {:.4f}".format(p3 * 100))

Percent chance the 000 message has one error: 10.0
Percent chance the 000 message has 2 or 3 errors: 2.8000

You can see here how the error percentage decreased a lot. By using repetition, we reduce the chance for error down to 2.8% from a whopping 10%. Although it’s just a made up error rate , we can change the value of p1 for different error rates and see how that scales with our code. For example for the error percentage of 5%, we have:



# Probability for a 000 message to contain one error:
p1 = 0.05

# Calculating the probability of a message containing 2 or 3 errors:
p3 = 3 * p1 * p1 * (1 - p1) + p1 * p1 * p1

print("Percent chance the 000 message has one error: {}".format(p1 * 100))
print("Percent chance the 000 message has 2 or 3 errors: {:.4f}".format(p3 * 100))

Percent chance the 000 message has one error: 5.0
Percent chance the 000 message has 2 or 3 errors: 0.7250

Here the error rate decreased down to 0.72% , which is a very big improvement over the initial 5% rate.

Now, we know that repetition can help, so we can implement this onto a simple program and see how it helps in that. We will start with a simple program that “encodes” a message, by transforming 1 bit to 3 bit, then “decodes” it by taking the 3 bit and turning it back to 1 bit. Let’s get it started:

Exercise 1

As a beginners exercise, construct a “decoder” which decodes the 3-bit state using three qubits, (0,1,2) into a single qubit (3) as per the given rules:

  • 000->0
  • 001->0
  • 010->0
  • 100->0
  • 111->1
  • 110->1
  • 101->1
  • 011->1

Taking a look at it, it’s clearly obvious that Toffoli gates on the qubits will give us the desired result, let’s construct the circuit:



# Creating a simple decoder for the classical case
def create_decoder(qr: QuantumRegister, cr: ClassicalRegister) -> QuantumCircuit:
# Expect a bit being encoded in the first 3 qubits and decode it into the 4th qubit
# Make sure values of the first 3 qubit stays the same

qc = QuantumCircuit(qr, cr)
q0, q1, q2, q3 = qr
(c0,) = cr

####### your code goes here #######

qc.ccx(q0, q1, q3)
qc.ccx(q1, q2, q3)
qc.ccx(q0, q2, q3)

return qc

Let’s see how the circuit looks like and whether we have made the correct circuit or not.

# We expect a bit being encoded in the first 3 qubits and decode it into the 4th qubit
qr = QuantumRegister(4)
cr = ClassicalRegister(1)

q0, q1, q2, q3 = qr

# To encode a 1. Change them to test the other encodings.
encoder = QuantumCircuit(qr, cr)
encoder.x(q0)
encoder.x(q1)
encoder.x(q2)

decoder = create_decoder(qr, cr)
qc1 = encoder.compose(decoder)

qc1.draw("mpl")
The complete encoder and decoder circuit. The encoding part is just applying X-gate on all qubits, and the decoding part is the application of Toffoli (ccx) gates on all the qubits, with target being the fourth qubit or q3.

Submit it to the grader:

# Submit your circuit
from qc_grader.challenges.spring_2023 import grade_ex4a

grade_ex4a(create_decoder(qr, cr))

The Quantum Case

In the classical case, having such error correction codes are relatively easy and simple, it’s just repetition of the bit several times. There are other more complex codes as well, but that is out of the scope of this exercise, nevertheless they end up using some kind of redundancies by repeating the information.

What makes the Quantum case so complicated?

In the quantum case, we can’t do what we are doing in the classical case, and that’s because of two main reasons:

  1. We cannot copy qubits which is stated by the no cloning theorem.
  2. Measuring a qubit will let its state collapse, which means we be careful when working with entangled qubits.

But, we can still do error correction. Everything comes with a price, and this error correction comes with the price of extra qubits to store the information and also a set of ancilla qubits, which are called stabilizers here.

The idea behind this is that these ancillas are not entangled with the qubits which have our state, but they still give us hints about the possible errors when being measured. Let’s see this in action. We will use two sets of qubits, one for encoding and one for the stabilizers.

Implementing a Bit-Flip Repetition code

Let’s get started:

# Setup a base quantum circuit for our experiments
encoding = QuantumRegister(3)
stabilizer = QuantumRegister(2)

encoding_q0, encoding_q1, encoding_q2 = encoding
stabilizer_q0, stabilizer_q1 = stabilizer

# Results of the encoding
results = ClassicalRegister(3)

result_b0, result_b1, result_b2 = results

# For measuring the stabilizer
syndrome = ClassicalRegister(2)

syndrome_b0, syndrome_b1 = syndrome

# The actual qubit which is encoded
state = encoding[0]

# The ancillas used for encoding the state
ancillas = encoding[1:]


# Initializing
def initialize_circuit() -> QuantumCircuit:
return QuantumCircuit(encoding, stabilizer, results, syndrome)

Initialize the Qubit

First prepare the quantum state:

We start by preparing:

initial_state = initialize_circuit()
initial_state.x(encoding[0])
initial_state.draw(output="mpl")
The first set is the encoding set and the second set is what we call the stabilizers or ancillas.

Encoding the Qubit

Following the footsteps of our classical counterparts, we need to make use of repetition in order to store the initial qubit.

So, what we want to do is to map our state :

The quantum state for the encoding.

using our encoding

The encoding unitary (operator) acting on our qubit state.

to the state

Encoded to a three qubit state.

As, we can see here, this state is the entangled state, and when 1 qubit is measured, we know the outcome of the other 2 qubits.

So an implementation of a CX gate will be used to create this entangled 3 qubit state from the initial 1 qubit state.

# Encoding using bit flip code
def encode_bit_flip(qc, state, ancillas):
qc.barrier(state, *ancillas)
for ancilla in ancillas:
qc.cx(state, ancilla)
return qc


# The circuit encoding our qubit
encoding_circuit = encode_bit_flip(initialize_circuit(), state, ancillas)

# The circuit including all parts so far
complete_circuit = initial_state.compose(encoding_circuit)
complete_circuit.draw(output="mpl")
The Encoding Circuit; The implementation of the CX gates map the one qubit state onto a three qubit entanglement and measuring only one qubit will be enough to let us know the state of other two qubits.

Preparing a Decoding Circuit

For the decoding the original state, we must build a decoder. Which does opposite of what our encoder was doing. Hence :

A decoder unitary acting on our state.

which maps

As it does exactly the opposite of what our encoder does, we can say:

One is just the complex conjugate of another.

Make the circuit:

# Encoding using bit flip code
def encode_bit_flip(qc, state, ancillas):
qc.barrier(state, *ancillas)
for ancilla in ancillas:
qc.cx(state, ancilla)
return qc


# The circuit encoding our qubit
encoding_circuit = encode_bit_flip(initialize_circuit(), state, ancillas)

# The circuit including all parts so far
complete_circuit = initial_state.compose(encoding_circuit)
complete_circuit.draw(output="mpl")
The decoder circuit: We can see it is exactly opposite of what our encoder circuit was.

Measuring the Stabilizers

We have seen above that we can entangle a qubit A with another qubit B by using a CX with A as a target and B as the source, (if B was already in superposition or entangled.)

Afterwards we can unentangle it again by using another CX on A as the target with either B as the source (or another qubit which is fully entangled and correlated with A.)

Since we want to measure our stabilizers to get an indication of potential errors which occurred, it is important that they are NOT entangled with the qubits encoding the state.

Knowing this fact, it is clear that we need an even number of CX gates applied to each stabilizer. Additional for the stabilizer to be useful, measuring it must tell us if a bit flip error occurred and on which of the 3 encoding qubits it happened.

This takes us to our next exercise.

Exercise 2

Compute syndrome bits, such that they can be measured to detect single bit flip errors. The code that will measure the syndrome bits is included and reset the stabilizer qubits to the 0 state.

There are different ways to do this, so let’s get a stabilizer with the simplest possible encoding:

  1. 00 -> No error occurred
  2. 01 -> An error occurred in qubit 0 (the first qubit)
  3. 10 -> An error occurred in qubit 1 (the second qubit)
  4. 11 -> An error occurred in qubit 2 (the third qubit)
# Add functions such that the classical bits can be used to see which qubit is flipped in the case a single qubit is flipped.
# Use 2 classical bits for it.
# 0 = 00 = no qubit flipped
# 1 = 01 = first qubit (qubit 0) flipped
# 2 = 10 second qubit (qubit 1) flipped
# 3 = 11 = third qubit (qubit 2) flipped
def measure_syndrome_bit(qc, encoding, stabilizer):
qc.barrier()
encoding_q0, encoding_q1, encoding_q2 = encoding
stabilizer_q0, stabilizer_q1 = stabilizer

####### your code goes here #######

qc.cx(encoding_q0, stabilizer_q0)
qc.cx(encoding_q2, stabilizer_q0)
qc.cx(encoding_q1, stabilizer_q1)
qc.cx(encoding_q2, stabilizer_q1)

####### don't edit the code below #######
qc.barrier()
qc.measure(stabilizer, syndrome)
with qc.if_test((syndrome_b0, 1)):
qc.x(stabilizer_q0)
with qc.if_test((syndrome_b1, 1)):
qc.x(stabilizer_q1)

return qc


syndrome_circuit = measure_syndrome_bit(initialize_circuit(), encoding, stabilizer)

complete_circuit = initial_state.compose(encoding_circuit).compose(syndrome_circuit)
complete_circuit.draw("mpl")

Submit it to the Grader

# Submit your circuit
from qc_grader.challenges.spring_2023 import grade_ex4b

grade_ex4b(complete_circuit

Correcting Errors

Now we can construct stabilizers and by measuring them we get the error syndromes. Of course, we do not only want to get indications if an error occurred, but we also want to be able to correct the errors.

We will use now dynamic circuits to use our syndrome measurements in order to correct potential errors. Similar to the classical case we can only correct at most 1 error, if we would want to correct more, we would need a longer code with 5 encoding qubits or more.

Since we chose our syndrome measurements above in a clever way, it should now be quite easy to correct the errors, since we know exactly which of the qubits is flipped, for the case of a single error.

Exercise 3

Correct the errors according to the measured syndromes

# Correct the errors, remember how we encoded the errors above!
def apply_correction_bit(qc, encoding, syndrome):
qc.barrier()
encoding_q0, encoding_q1, encoding_q2 = encoding

# Add your code here
for i in range(0, encoding.size):
with qc.if_test((syndrome, i + 1)):
qc.x(encoding[i])

return qc


correction_circuit = apply_correction_bit(initialize_circuit(), encoding, syndrome)
complete_circuit = (
initial_state.compose(encoding_circuit)
.compose(syndrome_circuit)
.compose(correction_circuit)
)
complete_circuit.draw(output="mpl")

The only part which is missing now is measuring the encoding qubits. We’ll apply the decoder circuit before measuring to recover the initial state.

If everything works perfectly with no errors, it would be enough to only measure our initial qubit, however, since this is not always the case we measure all qubits to see if something wrong happened.

def apply_final_readout(qc, encoding, results):
qc.barrier(encoding)
qc.measure(encoding, results)
return qc


measuring_circuit = apply_final_readout(initialize_circuit(), encoding, results)
complete_circuit = (
initial_state.compose(encoding_circuit)
.compose(syndrome_circuit)
.compose(correction_circuit)
.compose(decoding_circuit)
.compose(measuring_circuit)
)
complete_circuit.draw(output="mpl")
The complete circuit, including the encoder and decoder.
# Submit your circuit
from qc_grader.challenges.spring_2023 import grade_ex4c

grade_ex4c(complete_circuit

The Test

Now that we have everything we can test if we get the correct output.

We will do a first test without any errors to make sure that the implementation was correct:

# We first choose a simulator as our backend without any noise
backend = Aer.get_backend("qasm_simulator")
# We run the simulation and get the counts
counts = backend.run(complete_circuit, shots=1000).result().get_counts()
# And now we plot a histogram to see the possible outcomes and corresponding probabilities
plot_histogram(counts)

We can see that we get the correct results (it should give 00 001, since we initialized our initial qubit in the state 1).

As you can see the other qubits used in the encoding are in the 0 state after the process as expected.

Now we know that our circuit works without noise, so let’s add some noise!

For this we take a simulator which simulates the ibm_manila backend including noise:



# We choose a simulator for Manila a 5 qubit device, includes errors
backend = FakeManilaV2()
# We run the simulation and get the counts
counts = backend.run(complete_circuit, shots=1000).result().get_counts()
# We plot a histogram to see the possible outcomes and corresponding probabilities
plot_histogram(counts)

We got some wrong results as well.

We will get some wrong results, but overall, most results are correct. This is a good sign and means even with noise our code can work.

Still this does not tell us how good our scheme is, since we do not have a comparison to the case without error correction, so lets take a look on how good we would be without the error correction steps:

qc3 = (
initial_state.compose(encoding_circuit)
.compose(syndrome_circuit)
.compose(decoding_circuit)
.compose(measuring_circuit)
)


# We choose a again FakeManila
backend = FakeManilaV2()
# let the simulation run
counts = backend.run(qc3, shots=1000).result().get_counts()
# and plot our results
plot_histogram(counts)

We can see that the results are about the same, or even slightly worse since we do not use the encoding qubits after they are created.

When we would use these qubits for calculations, normally some errors would be introduced, this is not the case here.

On the other hand, the error correcting part can introduce errors, since it consists also of operations which take time.

For testing purposes, we build a circuit, which introduces some errors, but in a controlled way:

  • We want to introduce bit flip errors, since that is what we are correcting
  • We want that the errors on the 3 encoding qubits are independent of each other
  • We want that we can choose how high the probability is that errors are introduced
  • We want to have our input in percentage, and the output should be a circuit which generates errors with that probability.

Exercise 4

Create a circuit to add noise as defined above.

# Add some errors as defined above (only add errors to the encoding qubits)
def make_some_noise(qc, encoding, syndrome, error_percentage):
encoding_q0, encoding_q1, encoding_q2 = encoding
syndrome_b0, syndrome_b1 = syndrome

####### your code goes here #######
alpha = 2*math.asin(math.sqrt(error_percentage/100))
qc.rx(alpha, encoding_q0)
qc.rx(alpha, encoding_q1)
qc.rx(alpha, encoding_q2)

return qc


# Constructing a circuit with 10% error rate (for each of the encoding qubit)
noise_circuit = make_some_noise(initialize_circuit(), encoding, syndrome, 10)
noise_circuit.draw(output="mpl")

Submit it to the grader:

# Submit your circuit
from qc_grader.challenges.spring_2023 import grade_ex4d

grade_ex4d(noise_circuit)

We can now test how good our error correction works, when we introduce errors with a 10% probability:

qc4 = (
initial_state.compose(encoding_circuit)
.compose(noise_circuit)
.compose(syndrome_circuit)
.compose(correction_circuit)
.compose(decoding_circuit)
.compose(measuring_circuit)
)


# We choose again FakeManila
backend = FakeManilaV2()
# let the simulation run
counts = backend.run(qc4, shots=1000).result().get_counts()
# and plot our results
plot_histogram(counts)

We can see that our results got worse, but we still get 001 in most cases.

We now have successfully made our first error correction code and even tested it.

Our kind of error correction does of course only correct bit flip errors, if we also want to correct phase errors, we will need a different scheme and more qubits

Error Correction and Hardware Layout

We have used a simulator above, which already simulates the hardware, still if we want to run something more complex on a real device, we will also have to take the layout into consideration.

The layout of the device is important, since not all qubits are connected with each other, as in the ideal case, so we have to think about that, else a lot of swap operations will be used, which consist of 3 CX operations, which of course can also introduce errors.

Suppose we have the following line of qubits on our actual device

How would one map these physical qubits to the logical qubits we used above in our error correcting circuit?)

We assume here that the initial connecting of the qubits (entangling them) is “easier” than the error correction parts, since we might want to repeat the error correcting part several times, so only considering the error correction part how would you map these qubits?

With the code below you can see how different layouts can lead to different circuits.

from qiskit.circuit import IfElseOp

# Fill in a better layout to test!!
initial_layout = [0, 1, 2, 3, 4]

# We use Manila as our potential backend, since it has the wanted layout
backend = FakeManilaV2()

# Temporary workaround for fake backends. For real backends this is not required.
backend.target.add_instruction(IfElseOp, name="if_else")

# And now we transpile the circuit for the backend.
qc_transpiled = transpile(complete_circuit, backend, initial_layout=initial_layout)

qc_transpiled.draw()

No layout is perfect, especially since in the beginning to entangle the 3 qubits we need different connections than later for the error correction.

The layout [0,4,2,1,3] could sense, since then we have direct connections for all needed CX for the correction, and that part is potentially run several times.

We could also think about how we could do the initial setup (entangling) better with these qubits, since we could also use the qubits which are used as stabilizers first to construct the circuit.

To show you that it can also be quite different let’s look at the following layout how would you map the qubits?

Similar to above the layout [2,4,22,3,15] would make the sense, since then we have again direct connections for all needed CX.

We can see that there is not really a difference between these layouts, since both layouts are a simple line. If you would, however, look at the backend IBM_Quito you can see, that there are also other 5 qubit devices. We chose Manila since its layout is better suited for this exercise.

These small examples should show that on actual hardware you have to take care of the layout of the qubits!

This will be especially important in the bonus exercise!

Congratulations!

You made it to the end of Lab 4, you now have the working knowledge of quantum error correction and how to incorporate dynamic circuits into this.

Lab 4 of the IBM Quantum Challenge Spring 2023 provided participants with a comprehensive understanding of quantum error correction. By examining the application of error correction to classical data and delving into bit flip codes in quantum computing, participants gained hands-on experience in implementing error correction techniques. Furthermore, they explored the challenges and considerations of running error correction on real quantum devices, acquiring insights that will contribute to the advancement of practical quantum computing systems.

Stay tuned for our next and the final article in this series, where we will delve into Lab 5 of the IBM Quantum Challenge Spring 2023, using our knowledge of dynamic circuits and error correction to create a GHZ state and use the 127-qubit processor.

Happy error correcting!

--

--