# Quantum Error Correction

**The challenges with Quantum Computing — Part 5**

The majority of the algorithms that we have discussed so far assume that qubits are perfect and can be prepared in any state and manipulated with precision. Qubits that obey these assumptions are called logical qubits. However, with the current state of technology, the imperfections cannot be removed entirely. We refer to these imperfect qubits as physical qubits.

The quantum computers use these physical qubits despite the imperfections by using error mitigation effects. Error correction in quantum computing is crucial because quantum systems are extremely sensitive to external disturbances (noise), a phenomenon known as quantum decoherence. This sensitivity can lead to errors in quantum computations, which are a major challenge in building reliable quantum computers.

Two ways of mitigating error are repetition code and zero-noise extrapolation. We will explore repetition code by implementing a bit-flip repetition code shortly. A zero-noise extrapolation technique runs a quantum algorithm at different noise levels and then extrapolates the results to estimate the outcome in the absence of noise.

**Error Correction**

Whenever you send a message, even one as simple as a bit, there is always a possibility for small errors to occur and for the bit to flip from 0 to 1 or vice versa. In classical communication, it is easy to correct this error by using some repetition, so instead of sending 1, you can send 3 bits of data as 111. The receiver can now correct the error, if one bit is flipped. The actual error correction techniques in classical communication might be more complex and sophisticated, but the idea is to build some sort of redundancy.

In quantum computing, this is not easy as (i) we cannot copy qubits due to the no-cloning theorem, and (ii) measuring a qubit will let its state collapse. We will approach this problem by utilizing additional ancilla qubits, which we will use as stabilizers. The idea is that these ancillas are not itself entangled with the qubits which store the state but still provide us hints about possible errors when being measured.

Let’s see how we can implement a bit-flip repetition code in Qiskit. We will use the lab from IBM Quantum Spring Challenge 2023 to implement this.

**Implementing a Bit-flip Repetition Code**

Setting up circuit and initializing qubit:

`# 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)

# Initializing the qubit

initial_state = initialize_circuit()

initial_state.x(encoding[0])

initial_state.draw(output="mpl")

We will now encode the qubit. Similar to classical case, we will use repetition code in order to store the initial qubit, i.e. mapping our state |ψ⟩ = a|0⟩ + b|1⟩ to the state |ψ⟩ = a|000⟩ + b|111⟩. This is an entangled state, therefore when 1 qubit is measured the outcome of the other 2 qubits is also known:

`# 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")

We will now prepare a decoding circuit that will allow us to decode the state:

`# Decoding (doing the reverse)`

def decode_bit_flip(qc, state, ancillas):

qc.barrier(state, *ancillas)

for ancilla in ancillas:

qc.cx(state, ancilla)

return qc

decoding_circuit = decode_bit_flip(initialize_circuit(), state, ancillas)

decoding_circuit.draw(output="mpl")

We will now compute the syndrome bits (using our ancilla qubits), such that they can be measured to detect single bit-flip errors:

`# 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

# For measuring the stabilizer

qc.cx(encoding_q2, stabilizer_q0)

qc.cx(encoding_q0, stabilizer_q0)

qc.cx(encoding_q1, stabilizer_q1)

qc.cx(encoding_q2, stabilizer_q1)

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")

We will now use our dynamic circuit to use our syndrome measurement in order to correct potential errors:

`# Correcting the errors`

def apply_correction_bit(qc, encoding, syndrome):

qc.barrier()

encoding_q0, encoding_q1, encoding_q2 = encoding

with qc.if_test((syndrome_b0, 0)):

with qc.if_test((syndrome_b1,1)):

qc.x(encoding_q1)

with qc.if_test((syndrome_b0, 1)):

with qc.if_test((syndrome_b1,0)):

qc.x(encoding_q0)

with qc.if_test((syndrome_b0, 1)):

with qc.if_test((syndrome_b1,1)):

qc.x(encoding_q2)

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")

Let us now add the decoder to our circuit to measure our initial qubit:

`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")

This is what the final circuit looks like:

Let’s test our circuit on a simulator:

`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 (00 001), since we initialized our initial qubit in the state 1.

**Conclusion**

Quantum computing provides an exciting preview of what is to come. While the technology is at the stage currently where we cannot yet invent many useful applications, ongoing research and developments are expected to address current challenges. Continued progress in hardware, algorithms and error correction techniques is essential for unlocking the full potential of quantum computing. They are not expected to replace classical computers but to complement them, solving specific problems more efficiently.

This is Part 5 in a series of articles on quantum computing. The links to all the parts are below: