import numpy as np
import matplotlib.pyplot as plt
import time
from typing import *

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit.circuit.library.standard_gates import XGate
from qiskit.quantum_info import Statevector, Operator
from qiskit.visualization import plot_histogram, plot_bloch_multivector
sim = AerSimulator()

from util import zero, one

QI: Quantum Error Correction#

Throughout this series of notebooks, we have assumed logical qubits, i.e., qubits without errors. However, the qubits we have today cannot be controlled perfectly and thus have errors. These are called physical qubits. 2. We’ll first look at sources of error in a single qubit. 3. Then, we’ll look at how to use quantum error correction (QEC) to turn physical qubits into logical qubits. Specifically, we’ll look at the bit-flip code.

References

  1. Introduction to Classical and Quantum Computing, Chapter 4.7

Sources of Error#

Recall a single qubit.

plot_bloch_multivector(zero)
../_images/1a3de8fec85b8951ccb8d7fc4f4ddf6cd1e3590c2ad7484391bcf713f5b96dfe.png

YZ-Error or Bit-Flip Error#

  1. Instead of having a logical \(|0\rangle\), we may have a perturbed \(|0\rangle\) that is off in the \(YZ\) plane (rotation about x-axis) by a small angle.

  2. In this example, consider throwing off the \(|0\rangle\) by a \(\pi/20\) angle.

qc_yz_err = QuantumCircuit(1)
qc_yz_err.rx(np.pi/20, 0)
qc_yz_err.draw(output="mpl", style="iqp")
../_images/8358c69d9657fc73c79ffa5d4155f9a9ba6058de0eaefef3e662c0d40bf4a61d.png
plot_bloch_multivector(zero.evolve(Operator(qc_yz_err)))
../_images/9a85993b53fd1ce72af2395e7c0f6d91952d01380e0ecf999065a146a210edab.png

How bad is the error?#

We can compute the quantum state and compute the probability of measuring a \(|0\rangle\).

zero.evolve(Operator(qc_yz_err)).draw("latex")
\[0.9969173337 |0\rangle- 0.0784590957 i |1\rangle\]
# Probability of measuring a |0>
0.9969173337**2
0.9938441702315172

As a histogram#

qc = QuantumCircuit(1, 1)
qc.rx(np.pi/20, 0)        
qc.measure(0, 0)
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
plot_histogram(answer)
../_images/ada6c298b5d166972b4e050093decebe2e9ef6727eaf1c29bdff8524cc081f0e.png

How small is small?#

  1. You might think that \(0.9938441702315172\) is quite a high probability.

  2. Let’s plot the number of successive operations we can perform and the probability of success without error.

  3. For comparison, your classical computer can effectively run billions of operations without errors every second.

p = 0.9938441702315172
num_ops = range(1, 1000, 10)
prob_err = [p**x for x in num_ops]
plt.plot(num_ops, prob_err)
plt.title("Probability of success as a function of number of operations")
plt.ylabel("Probability");
plt.xlabel("Number of operations");
../_images/67a63bd49524c0e04cd86325a6b925a8636756ff077672b74d3790b33b3c9c1b.png
p = 0.999999  # Improving the error rate
num_ops = range(1, 1000, 10)
prob_err = [p**x for x in num_ops]
plt.plot(num_ops, prob_err)
plt.title("Probability of success as a function of number of operations")
plt.ylabel("Probability");
plt.xlabel("Number of operations");
../_images/82035205a463485103ebc6b5469a75936361da65cbd94edad50f4155f8e3fa5e.png
qc_yz_err2 = QuantumCircuit(1)
qc_yz_err2.rx(np.pi/1800, 0)
zero.evolve(Operator(qc_yz_err2)).draw("latex")
\[0.9999996192 |0\rangle- 0.0008726645 i |1\rangle\]
0.9999996192**2
0.999999238400145
# Your qubit needs to be accurate to within 0.1 degrees in order
# to perform 1000 operations with probability 0.999 of success
print("Angle in degrees", np.pi/1800*(180/np.pi))
Angle in degrees 0.1

XY-Error or Phase-Flip Error#

  1. Unlike classical error correction where we can only flip a \(0\) to a \(1\), or vice versa, we also have phase-flip errors.

  2. This is perturbation of a quantum state in the \(XY\)-plane, i.e., rotation about the Z-axis in the Bloch sphere.

  3. Consider throwing off the \(H|0\rangle\) by a \(\pi/20\) angle in the \(XY\) plane. We are using \(H|0\rangle\) since rotation of \(|0\rangle\) in the z-axis.

qc_xy_err = QuantumCircuit(1)
qc_xy_err.h(0)
qc_xy_err.rz(np.pi/20, 0)
qc_xy_err.draw(output="mpl", style="iqp")
../_images/d4f526a984c2986608306728ab4b19e8586f9549fac3b08a5ca75ec51d5ccb7b.png
plot_bloch_multivector(zero.evolve(Operator(qc_xy_err)))
../_images/5c345cf1250d74dac6b3b87b5549f67df80f0d145724ad6541f812845bdc9e78.png
zero.evolve(Operator(qc_xy_err)).draw("latex")
\[(0.704927007 - 0.0554789586 i) |0\rangle+(0.704927007 + 0.0554789586 i) |1\rangle\]
# Probability of measuring a |0>
np.abs(0.704927007 - 0.0554789586*1j)**2
np.float64(0.5000000000453187)

Discusion question#

Is it possible to measure the error in this qubit? How would we be able to tell that there is an error in this qubit?

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.rz(np.pi/20, 0)        
qc.measure(0, 0)
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
plot_histogram(answer)
../_images/66e4737fedcb9af0d852bf6261731e2a3758ebb4e9a1b3ca7ced9ff85bb9874c.png

Bit-Flip Error and Phase-Flip Error#

A single qubit can exhibit both bit-flip and phase-flip errors.

qc_err = QuantumCircuit(1)
qc_err.rx(np.pi/20, 0)
qc_err.rz(np.pi/20, 0)
qc_err.draw(output="mpl", style="iqp")
../_images/8589bc4a0e838d01852bdb4b0f469c36a2ed779bc271b8264a1e77cdcc1392a9.png
plot_bloch_multivector(zero.evolve(Operator(qc_err)))
../_images/d90c03f8c7a053ddb55c09e72d23e2df69c14e86bde6e67a217b66cdfc7dcd08.png
zero.evolve(Operator(qc_err)).draw("latex")
\[(0.9938441703 - 0.0782172325 i) |0\rangle+(0.0061558297 - 0.0782172325 i) |1\rangle\]
np.abs(0.9938441703 - 0.0782172325*1j)**2
np.float64(0.9938441702992548)

Histogram#

qc = QuantumCircuit(1, 1)
qc.rx(np.pi/20, 0)        
qc.measure(0, 0)
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
plot_histogram(answer)
../_images/e07aa66eaeb5d385052310e54677149478f2358209215a66225ec5695df4f59c.png

Error Correction Strategy#

  1. Introduce Bit-Flip Code which corrects errors in the YZ plane of the Bloch sphere.

  2. The Phase-Flip Code which corrects errors in the XY plane of the Bloch sphere is similar to the Bit-Flip code.

  3. Shor’s Code combines the Bit-Flip Code and Phase-Flip Code to correct both flip and phase errors.

Bit-Flip Code#

In the Bit-Flip Code, we will use three physical qubits to encode one logical qubit.

  1. Logical \(|0_L\rangle\) is defined as

\[ |0_L\rangle = |000\rangle \,. \]
  1. Logical \(|1_L\rangle\) is defined as

\[ |1_L\rangle = |111\rangle \,. \]
  1. Thus any logical qubit can be defined as

\[ \alpha|0_L\rangle + \beta|1_L\rangle \,. \]

Example: Entire Qubit Flipped#

  1. Suppose we have a qubit

\[ |\psi\rangle = \alpha|0_L\rangle + \beta|1_L\rangle \]

where one of the underlying qubits becomes corrupted. For example,

\[ \alpha|000\rangle + \beta|111\rangle \rightarrow \alpha|100\rangle + \beta|011\rangle \]

so that the most-significant physical qubit becomes flipped.

  1. Intuitively, our error-correction scheme is: predict the majority of a qubit in a group since flipping two \(|0\rangle\)’s to \(|1\rangle\)’s is less likely than flipping a single one.

  2. However, how do we do this without measuring the qubit since doing so would destroy the state of superposition?

Majority Error-Correcting Code#

  1. Suppose we have \(|b_2 b_1 b_0\rangle\).

  2. We want \(|001\rangle\), \(|010\rangle\), and \(|100\rangle\) to be corrected to \(|000\rangle\).

  3. Similarly we want \(|110\rangle\), \(|101\rangle\), and \(|011\rangle\) to be corrected to \(|111\rangle\).

Parity and Majority Error-Correcting Code#

We can accomplish this by computing the parity of adjacent bits \(b_2 \oplus b_1\) and \(b_1 \oplus b_0\).

Codeword

b2 XOR b1

b1 XOR b0

000

0

0

001

0

1

010

1

1

011

1

0

100

1

0

101

1

1

110

0

1

111

0

0

  1. Parity of \(00\) means no error (either \(|000\rangle\) or \(|111\rangle\)).

  2. Parity of \(01\) means \(b_0\) flipped (either \(|001\rangle\) or \(|110\rangle\)).

  3. Parity of \(10\) means \(b_2\) flipped (either \(|011\rangle\) or \(|100\rangle\)).

  4. Parity of \(11\) means \(b_1\) flipped (either \(|010\rangle\) or \(|101\rangle\)).

Crucially, we do not need to know the codeword! This means we can implement it in a quantum setting without measurement.

Quantum Parity and Majority Error-Correcting Code#

Recall the CNOT gate calculates an XOR.

\[ CNOT|b_1 b_0\rangle = CNOT|b_1 (b_1 \oplus b_0)\rangle \]

We would thus like to construct a circuit \(U\) such that

\[ U_\text{parity}|q_4 q_3 q_2 q_1 q_0\rangle = |(q_2 \oplus q_1) (q_1 \oplus q_0) q_2 q_1 q_0\rangle \,. \]
qc_parity = QuantumCircuit(5)
qc_parity.cx(0, 3); qc_parity.cx(1, 3)  # Compute |q_3> = |q_1 XOR q_0>
qc_parity.cx(1, 4); qc_parity.cx(2, 4)  # Compute |q_4> = |q_2 XOR q_1>
qc_parity.draw(output="mpl", style="iqp")
../_images/6320bc9541e5594cc5c04b14240408e7733586848ee46f21bce0fbf60a04fe3c.png
# |q_3 q_3 q_2 q_1 q_0>
# |q_2 q_1 q_0> = |000>
# |q_4 q_3> = |(q_2 XOR q_1) (q_1 XOR q_0)> = 00
# This means there are no errors
(zero ^ zero ^ zero ^ zero ^ zero).evolve(Operator(qc_parity)).draw("latex")
\[ |00000\rangle\]
# |q_3 q_3 q_2 q_1 q_0>
# |q_2 q_1 q_0> = |010>
# |q_4 q_3> = |(q_2 XOR q_1) (q_1 XOR q_0)> = 11
# Parity of 11 means q_1 flipped (either |010> or |101>).
(zero ^ zero ^ zero ^ one ^ zero).evolve(Operator(qc_parity)).draw("latex")
\[ |11010\rangle\]
# |q_3 q_3 q_2 q_1 q_0>
# |q_2 q_1 q_0> = |011>
# |q_4 q_3> = |(q_2 XOR q_1) (q_1 XOR q_0)> = 10
# Parity of 10 means q_2 flipped (either |011> or |100>).
(zero ^ zero ^ zero ^ one ^ one).evolve(Operator(qc_parity)).draw("latex")
\[ |10011\rangle\]

Correcting the parity#

  1. Recall that a parity of \(11\) means \(q_1\) flipped (either \(|010\rangle\) or \(|101\rangle\)). Thus we need a circuit

\[ U_{11}|11q_2 q_1 q_0\rangle = U_{11}|11q_2 (X q_1) q_0\rangle \,. \]
  1. Recall that a parity of \(10\) means \(q_2\) flipped (either \(|011\rangle\) or \(|100\rangle\)). Thus we need a circuit

\[ U_{10}|10q_2 q_1 q_0\rangle = U_{10}|11(X q_2) q_1 q_0\rangle \,. \]
  1. Finally recall that a parity of \(01\) means that \(q_0\) flipped (either \(|001\rangle\) or \(|110\rangle\)). Thus we need a circuit

\[ U_{01}|01q_2 q_1 q_0\rangle = U_{01}|11 q_2 q_1 (X q_0) \rangle \,. \]
qc_fix = QuantumCircuit(5)
qc_fix.append(XGate().control(2), [3, 4, 1])                  # |11> -> flip q_1
qc_fix.barrier()
qc_fix.x(4); qc_fix.append(XGate().control(2), [3, 4, 0]); qc_fix.x(4)     # |10> -> flip q_0
qc_fix.barrier()
qc_fix.x(3); qc_fix.append(XGate().control(2), [3, 4, 2])     # |01> -> flip q_2
qc_fix.draw(output="mpl", style="iqp")
../_images/689c67c81d90283fe51559ccdd208c34211c93efc18942f0be53c998c07b4803.png

Putting it together#

def create(q_2, q_1, q_0):
    qc = QuantumCircuit(5, 5)
    qc.initialize(q_0, 0); qc.initialize(q_1, 1); qc.initialize(q_2, 2);
    qc.barrier()
    
    # Compute parity
    qc.cx(0, 3); qc.cx(1, 3)   # Compute |q_3> = |q_1 XOR q_0>
    qc.cx(1, 4); qc.cx(2, 4)   # Compute |q_4> = |q_2 XOR q_1>
    qc.barrier()
    qc.measure(3, 3); qc.measure(4, 4)
    qc.barrier()
    
    # Fix parity
    qc.append(XGate().control(2), [3, 4, 1])              # |11> -> flip q_1
    qc.barrier()
    qc.x(4); qc.append(XGate().control(2), [3, 4, 0]); qc.x(4)     # |10> -> flip q_0
    qc.barrier()
    qc.x(3); qc.append(XGate().control(2), [3, 4, 2])     # |01> -> flip q_2
    qc.barrier()
    
    # Reinitialize for further error correction
    qc.initialize(zero, 3)
    qc.initialize(zero, 4)
    
    # Readout
    qc.measure(0, 0); qc.measure(1, 1); qc.measure(2, 2)  # Measure result
    return qc
qc = create(one, one, zero)
qc.draw(output="mpl", style="iqp")
../_images/9cf566f3044ea16ff467b43663dfda2b36b461f336eb4ff6234d8c437f3ff944.png
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
# Note that |01111> = |01> \oplus |111> so that |110> got corrected to |111>
plot_histogram(answer)
../_images/660ea7f5d37f98af5a76dad7d178bb5b496f699cc10c6602eae81849be747473.png
qc = create(zero, one, zero)
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
# Note that |11000> = |11> \oplus |000> so that |010> got corrected to |000>
plot_histogram(answer)
../_images/c6a4d743d4f2258db260107822f2b185b23b6c166e5004199ed964e01632e413.png

What about partial errors?#

Suppose we corrupt a qubit by a rotation of \(\theta\) about the x-axis (\(YZ\) plane). This corresponds to using the \(R_x(\theta)\) gate.

\[\begin{split} R_x(\theta) = \begin{pmatrix} \cos(\theta/2) & -i\sin(\theta/2) \\ -i\sin(\theta/2) & \cos(\theta/2) \end{pmatrix} \end{split}\]

Reparameterizing as \(\epsilon = \sin (\theta/2)\), we obtain

\[\begin{split} U_\epsilon = \begin{pmatrix} \sqrt{1 - \epsilon^2} & -i \epsilon \\ -i \epsilon & \sqrt{1 - \epsilon^2} \end{pmatrix} \,. \end{split}\]

Example: suppose most-significant qubit is corrupted#

This produces a physical qubit in state

(2)#\[\begin{align} (U_\epsilon \otimes I \otimes I)(\alpha|000\rangle + \beta|111\rangle) & = \alpha (\sqrt{1 - \epsilon^2}|000\rangle -i \epsilon |100\rangle) + \beta (\sqrt{1 - \epsilon^2}|111\rangle - i\epsilon|011\rangle) \\ & = \alpha\sqrt{1 - \epsilon^2}|000\rangle - i\alpha\epsilon|100\rangle + \beta\sqrt{1 - \epsilon^2}|111\rangle - i\beta\epsilon|011\rangle \,. \end{align}\]
  1. \(parity(q_2, q_1) = 0\) and \(parity(q_1, q_0) = 0\) occurs with the state

\[ \alpha\sqrt{1 - \epsilon^2}|000\rangle + \beta\sqrt{1 - \epsilon^2}|111\rangle \,. \]

This occurs with probability \(|\alpha\sqrt{1 - \epsilon^2}|^2 + |\beta\sqrt{1 - \epsilon^2}|^2 = 1 - \epsilon^2\). Thus, with probability \(1 - \epsilon^2\), we do not need to do anything.

  1. \(parity(q_2, q_1) = 1\) and \(parity(q_1, q_0) = 0\) occurs with the state

\[ -i\beta\epsilon|011\rangle - i\alpha\epsilon|100\rangle \,. \]

This occurs with probability \(|-i\beta\epsilon|^2 + |-i\alpha\epsilon|^2 = \epsilon^2\). Thus, with probability \(\epsilon^2\), we should apply an \(X\) gate to the most-significant qubit.

  1. The reasoning for partial flips on the middle and least significant qubit is similar.

  2. Thus this procedure is exactly the same as the complete flip case.

def corrupt(angle, q):
    qc_err = QuantumCircuit(1)
    # qc_err.p(np.pi, 0)
    qc_err.rx(angle * np.pi/180, 0)
    return q.evolve(Operator(qc_err))

plot_bloch_multivector(corrupt(90, zero))
../_images/fd2e4f1f9388383dcf87f7259bffb63fe9f1d90a0c45473c5154a6fb1eae7545.png
qc = create(one, one, corrupt(90, one))
results = sim.run(qc, shots=1024).result()
answer = results.get_counts()
plot_histogram(answer)
../_images/2b597cdbb2051d3d8246a7464ab701f636a28db20fe73c8f0d0e0462998329fa.png

Summary#

  1. We saw the basics of quantum error correction based on bit-flip code.

  2. The bit-flip code encodes 1 logical qubit with 3 physical qubits. 2 ancila (additional) qubits in addition to mid-circuit measurement were needed to complete the code.

  3. Correcting bit-flips and phase-flips together can be done with Shor’s code (see Introduction to Classical and Quantum Computing, Chapter 4.7 for more details)

  4. It is an active area of research of how to develop better quantum error correcting codes that have lower overhead or can correct different kinds of errors.

  5. Logical qubits are required to implement Grover’s search and Shor’s algorithm