import numpy as np

import qiskit.quantum_info as qi
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.quantum_info import Statevector, Operator
from qiskit.visualization import plot_histogram
sim = AerSimulator()

from util import zero, one, measure_outcome, Pretty

QI: Quantum Teleportation#

In this notebook, we’ll introduce quantum teleportation. It is a protocol that enables one party to send a qubit to another party using one shared entangled qubit and two classical bits. Importantly, this protocol does not violate no cloning.

References

  1. https://learn.qiskit.org/course/ch-algorithms/quantum-teleportation

  2. Introduction to Quantum Information Science: Lecture 10 by Scott Aaronson

  3. Quantum Computation and Quantum Information: Chapter 1.3, Nielsen and Chuang

Problem#

  1. Two parties, one named Alice and another named Bob.

  2. Question: Can Alice send a qubit to Bob? That is, after the end of sending her qubit, she no longer has access to it.

  3. Answer: yes, but only if Alice and Bob share an entangled qubit and uses two classical bits.

# Protocol uses 3 qubits and 2 classical bits in 2 different registers
qr_alice = QuantumRegister(2, name="q_Alice")
qr_bob = QuantumRegister(1, name="q_Bob")
crz, crx = ClassicalRegister(1, name="crz"), ClassicalRegister(1, name="crx")
teleportation_circuit = QuantumCircuit(qr_alice, qr_bob, crz, crx)

Step 1: Charlie creates entangled state#

Suppose Alice wants to transmit

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

We will start with an initial quantum state

\[\begin{align*} |psi_0\rangle & = |q_2 q_1 q_0\rangle \\ & = |00\rangle \otimes |\psi\rangle \end{align*}\]

Alice will own \(q_0\) and \(q_1\), Bob will own \(q_2\), and Charlie will entangle qubits \(q_1\) and \(q_2\).

def create_bell_pair(qc, a, b):
    qc.h(a)     # Put qubit a into state |+>
    qc.cx(a, b) # CNOT with a as control and b as target
# Step 1: Charlie creates entangled state
create_bell_pair(teleportation_circuit, 1, 2)   # qubits 1 and 2 are entangled now
teleportation_circuit.draw(output="mpl", style="iqp")
../_images/eb3323d50837b23fae7d92ce774f203bf54bfed9a5202225c79dbce8160283ca.png

State after step 1#

After we entangle qubits \(|q_1\rangle\) and \(|q_2\rangle\), we obtain the state

\[\begin{align*} |\psi_1\rangle & = (((H \otimes I) CNOT) \otimes I) |\psi\rangle \tag{definition} \\ & = (\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) \otimes |\psi\rangle \tag{Bell state} \\ & = (\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) \otimes (\alpha|0\rangle + \beta|1\rangle) \tag{definition} \\ & = \frac{1}{\sqrt{2}}(\alpha|000\rangle + \alpha|110\rangle + \beta|001\rangle + \beta|111\rangle) \tag{calculation} \,. \end{align*}\]
alpha = 1/np.sqrt(3)
beta = np.sqrt(2/3)
psi = alpha*zero + beta*one
(zero ^ zero ^ psi).evolve(teleportation_circuit).draw("latex")
\[\frac{\sqrt{6}}{6} |000\rangle+\frac{\sqrt{3}}{3} |001\rangle+\frac{\sqrt{6}}{6} |110\rangle+\frac{\sqrt{3}}{3} |111\rangle\]

Step 2: Alice’s computation#

Alice applies

\[ (I \otimes I \otimes H)(I \otimes CNOT) \]

to the result of step 1.

def alice_gates(qc, q, a):
    qc.cx(q, a)
    qc.h(q)
## Step 2
teleportation_circuit.barrier(label="Step 1 | Step 2") # Use barrier to separate steps
alice_gates(teleportation_circuit, 0, 1)
teleportation_circuit.draw(output="mpl", style="iqp")
../_images/0b38853b6958c02e917499b1943111d53baa90cd31fbfa71621e2126ac78a743.png

State after step 2#

After Alice performs here computation, we obtain the quantum state

\[\begin{align*} |\psi_2\rangle & = (I \otimes I \otimes H)(I \otimes CNOT) |\psi_1\rangle \tag{step 2} \\ & = (I \otimes I \otimes H)(I \otimes CNOT)(\frac{1}{\sqrt{2}}(\alpha|000\rangle + \alpha|110\rangle + \beta|001\rangle + \beta|111\rangle) \tag{definition $\psi_1\rangle$} \\ & = (I \otimes I \otimes H)(\frac{1}{\sqrt{2}}(\alpha|000\rangle + \alpha|110\rangle + \beta|011\rangle + \beta|101\rangle) \tag{CNOT} \\ & = \frac{1}{2}\alpha(|000\rangle + |110\rangle + |001\rangle + |111\rangle) + \frac{1}{2}\beta(|010\rangle + |100\rangle - |011\rangle - |101\rangle) \tag{H} \\ & = \frac{1}{2}((\alpha|0\rangle + \beta|1\rangle)|00\rangle + (\alpha|1\rangle + \beta|0\rangle)|10\rangle + (\alpha|0\rangle - \beta|1\rangle)|01\rangle + (\alpha|1\rangle - \beta|0\rangle)|11\rangle) \tag{factoring} \,. \end{align*}\]
(zero ^ zero ^ psi).evolve(teleportation_circuit).draw('latex')
\[\frac{\sqrt{3}}{6} |000\rangle+\frac{\sqrt{3}}{6} |001\rangle+\frac{\sqrt{6}}{6} |010\rangle- \frac{\sqrt{6}}{6} |011\rangle+\frac{\sqrt{6}}{6} |100\rangle- \frac{\sqrt{6}}{6} |101\rangle+\frac{\sqrt{3}}{6} |110\rangle+\frac{\sqrt{3}}{6} |111\rangle\]
# Save the circuit before measurement
U_steps_1_to_2 = Operator(teleportation_circuit)

Step 3: Transmission#

  1. Alice measures qubit \(0\) and qubit \(1\), and sends them as classical bits to Bob.

  2. Recall that qubit \(1\) is entangled with qubit \(2\), and that Bob has access to qubit \(2\).

def measure_and_send(qc, a, b):
    qc.measure(a, 0)
    qc.measure(b, 1)
# Step 3: Transmission
teleportation_circuit.barrier(label="Step 2 | Step 3")
measure_and_send(teleportation_circuit, 0, 1)
teleportation_circuit.draw(output="mpl", style="iqp")
../_images/4af76ef2b942965282fba0cce0efacdd14e036050b0a8d2496ffe321881a71c6.png

Effect on quantum state#

As a reminder,

\[ |\psi_2\rangle = \frac{1}{2}((\alpha|0\rangle + \beta|1\rangle)|00\rangle + (\alpha|1\rangle + \beta|0\rangle)|10\rangle + (\alpha|0\rangle - \beta|1\rangle)|01\rangle + (\alpha|1\rangle - \beta|0\rangle)|11\rangle \,. \]

After measurement, Alice will have

  1. \(|00\rangle\),

  2. \(|10\rangle\),

  3. \(|01\rangle\), and

  4. \(|11\rangle\) all with equal probability.

Bob will have

  1. \((\alpha|0\rangle + \beta|1\rangle)\) if Alice measures \(|00\rangle\),

  2. \((\alpha|1\rangle + \beta|0\rangle)\) if Alice measures \(|10\rangle\),

  3. \((\alpha|0\rangle - \beta|1\rangle)\) if Alice measures \(|01\rangle\), and

  4. \((\alpha|1\rangle - \beta|0\rangle)\) if Alice measure \(|11\rangle\).

We can use density matrices to check these results.

# The state before step 3
v = (zero ^ zero ^ psi).evolve(U_steps_1_to_2)
v.draw("latex")
\[\frac{\sqrt{3}}{6} |000\rangle+\frac{\sqrt{3}}{6} |001\rangle+\frac{\sqrt{6}}{6} |010\rangle- \frac{\sqrt{6}}{6} |011\rangle+\frac{\sqrt{6}}{6} |100\rangle- \frac{\sqrt{6}}{6} |101\rangle+\frac{\sqrt{3}}{6} |110\rangle+\frac{\sqrt{3}}{6} |111\rangle\]
# The density matrix before step 3
rho = qi.DensityMatrix(v)
rho.draw("latex")
\[\begin{split}\begin{bmatrix} \frac{1}{12} & \frac{1}{12} & 0.1178511302 & -0.1178511302 & 0.1178511302 & -0.1178511302 & \frac{1}{12} & \frac{1}{12} \\ \frac{1}{12} & \frac{1}{12} & 0.1178511302 & -0.1178511302 & 0.1178511302 & -0.1178511302 & \frac{1}{12} & \frac{1}{12} \\ 0.1178511302 & 0.1178511302 & \frac{1}{6} & - \frac{1}{6} & \frac{1}{6} & - \frac{1}{6} & 0.1178511302 & 0.1178511302 \\ -0.1178511302 & -0.1178511302 & - \frac{1}{6} & \frac{1}{6} & - \frac{1}{6} & \frac{1}{6} & -0.1178511302 & -0.1178511302 \\ 0.1178511302 & 0.1178511302 & \frac{1}{6} & - \frac{1}{6} & \frac{1}{6} & - \frac{1}{6} & 0.1178511302 & 0.1178511302 \\ -0.1178511302 & -0.1178511302 & - \frac{1}{6} & \frac{1}{6} & - \frac{1}{6} & \frac{1}{6} & -0.1178511302 & -0.1178511302 \\ \frac{1}{12} & \frac{1}{12} & 0.1178511302 & -0.1178511302 & 0.1178511302 & -0.1178511302 & \frac{1}{12} & \frac{1}{12} \\ \frac{1}{12} & \frac{1}{12} & 0.1178511302 & -0.1178511302 & 0.1178511302 & -0.1178511302 & \frac{1}{12} & \frac{1}{12} \\ \end{bmatrix} \end{split}\]
# Checking that we do indeed have a pure state
np.trace(rho.data @ rho.data)
np.complex128(0.9999999999999991+0j)

Recall partial measurement on density matrix#

Let \(\{ M_m \}_m\) be a set of measurement operators where \(m\) refers to the outcome so that it satisfies

\[ \sum_m M_m^\dagger M_m = I \,. \]

Then the probability of obtaining outcome \(m\) after measuring is

\[ p(m) = tr(M_m^\dagger M_m \rho) \]

and the state of the system after measurement is

\[ \frac{M_m \rho M_m^\dagger}{tr(M_m^\dagger M_m \rho)} \,. \]
zzz = zero ^ zero ^ zero; zzo = zero ^ zero ^ one;
zoz = zero ^ one ^ zero; zoo = zero ^ one ^ one;
ozz = one ^ zero ^ zero; ozo = one ^ zero ^ one;
ooz = one ^ one ^ zero; ooo = one ^ one ^ one;

# Construct partial measurements
Pi00 = np.outer(zzz, zzz) + np.outer(ozz, ozz)
Pi01 = np.outer(zzo, zzo) + np.outer(ozo, ozo)
Pi10 = np.outer(zoz, zoz) + np.outer(ooz, ooz)
Pi11 = np.outer(zoo, zoo) + np.outer(ooo, ooo)
Pretty(Pi00 + Pi01 + Pi10 + Pi11)
\[\begin{split}\begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ \end{bmatrix} \end{split}\]

Checking the results when 00 is measured.

p_00, rho_00 = measure_outcome(rho, Pi00)
print(p_00)
Pretty(rho_00)
(0.2499999999999999+0j)
\[\begin{split}\begin{bmatrix} \frac{1}{3} & 0 & 0 & 0 & \frac{\sqrt{2}}{3} & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \frac{\sqrt{2}}{3} & 0 & 0 & 0 & \frac{2}{3} & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{bmatrix} \end{split}\]
bob_00 = alpha * zero + beta * one
np.allclose(rho_00, np.outer((bob_00 ^ zero ^ zero), (bob_00 ^ zero ^ zero)))
True

Checking the results when 01 is measured.

p_01, rho_01 = measure_outcome(rho, Pi01)
print(p_01)
Pretty(rho_01)
(0.2499999999999999+0j)
\[\begin{split}\begin{bmatrix} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & \frac{1}{3} & 0 & 0 & 0 & - \frac{\sqrt{2}}{3} & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & - \frac{\sqrt{2}}{3} & 0 & 0 & 0 & \frac{2}{3} & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{bmatrix} \end{split}\]
bob_01 = alpha * zero - beta * one
np.allclose(rho_01, np.outer((bob_01 ^ zero ^ one), (bob_01 ^ zero ^ one)))
True

Step 4: Bob’s computation#

Bob now needs to reconstruct Alice’s qubit \(|q_1\rangle\) in his qubit \(|q_2\rangle\) using the two classical bits that Alice has provided. As a reminder, Bob at this point has

  1. \((\alpha|0\rangle + \beta|1\rangle)\) if Alice measures \(|00\rangle\),

  2. \((\alpha|1\rangle + \beta|0\rangle)\) if Alice measures \(|10\rangle\),

  3. \((\alpha|0\rangle - \beta|1\rangle)\) if Alice measures \(|01\rangle\), and

  4. \((\alpha|1\rangle - \beta|0\rangle)\) if Alice measure \(|11\rangle\).

The trick to reconstruct \(|\psi\rangle = \alpha|0\rangle + \beta|1\rangle\) is to realize that we just need to use the cloassical bits, i.e., the results of Alice’s measurement, to correct the phases of our quantum state. In particular,

  1. no correction if \(|00\rangle\) is received,

  2. a flip (X) if \(|10\rangle\) is received,

  3. a phase flip (Z) if \(|01\rangle\) is received, and

  4. a flip (X) and phase flip (Z) if \(|11\rangle\) is received.

We give this circuit now.

def bob_gates(qc, qubit, crz, crx):
    # Here we use c_if to control our gates with a classical
    # bit instead of a qubit
    qc.x(qubit).c_if(crx, 1) # Apply gates if the registers 
    qc.z(qubit).c_if(crz, 1) # are in the state '1'
## Step 4: Bob's computation
teleportation_circuit.barrier(label="Step 3 | Step 4") # Use barrier to separate steps
bob_gates(teleportation_circuit, 2, crz, crx)
teleportation_circuit.draw(output="mpl", style="iqp")
../_images/9b1f41a0b705c6baec17d506c85f876caa4423fff890e2967a08fe2ad888ccca.png

State after step 4.#

After decoding, Bob has

  1. \((\alpha|0\rangle + \beta|1\rangle)\) becomes \((\alpha|0\rangle + \beta|1\rangle)\),

  2. \((\alpha|1\rangle + \beta|0\rangle)\) becomes \(X(\alpha|1\rangle + \beta|0\rangle) = \alpha|0\rangle + \beta|1\rangle\),

  3. \((\alpha|0\rangle - \beta|1\rangle)\) becomes \(Z(\alpha|0\rangle - \beta|1\rangle) = \alpha|0\rangle + \beta|1\rangle\), and

  4. \((\alpha|1\rangle - \beta|0\rangle)\) becomes \(ZX(\alpha|1\rangle - \beta|0\rangle) = \alpha|0\rangle + \beta|1\rangle\).

Thus Bob has indeed received the qubit \(|\psi\rangle\) from Alice. Moreover, Alice’s knowledge of \(|\psi\rangle\) has been destroyed.

Putting it together#

We gather all the pieces together into a single function to enable more experimentation.

def quantum_teleportation(psi: Statevector) -> QuantumCircuit:
    # Create circuit
    qr = QuantumRegister(3, name="q")
    crz, crx = ClassicalRegister(1, name="crz"), ClassicalRegister(1, name="crx")
    qc_tele = QuantumCircuit(qr, crz, crx)
    
    # Initialize qubit 0 to the message that Alice wants to send
    qc_tele.initialize(psi, 0)
    
    # Step 1: Third party creates entangled state
    create_bell_pair(qc_tele, 1, 2)
    qc_tele.barrier(label="Step 1 | Step 2")
    
    # Step 2: Alice's computation
    alice_gates(qc_tele, 0, 1)
    qc_tele.barrier(label="Step 2 | Step 3")

    # Step 3: Transmit
    measure_and_send(qc_tele, 0 ,1)
    qc_tele.barrier(label="Step 3 | Step 4")
    
    # Step 4: Bob's computation
    bob_gates(qc_tele, 2, crz, crx)
    
    return qc_tele
alpha = 1/np.sqrt(3); beta = np.sqrt(2/3)
psi = alpha*zero + beta*one
qc_tele = quantum_teleportation(psi)
qc_tele.draw(output="mpl", style="iqp")
../_images/05e93ae0915bd1fb5e91dd06afdfbd5a0d920ed4b65288bb8495f627acbb1481.png
cr_result = ClassicalRegister(1)
qc_tele.add_register(cr_result)
qc_tele.measure(2,2)
qc_tele.draw(output="mpl", style="iqp")
../_images/ac6b870a60eec02bd7440b9816b4ade9823951671ec3303a95edd3097eb1aa74.png
result = sim.run(qc_tele).result()
counts = result.get_counts(qc_tele)
plot_histogram(counts)
../_images/097e392cf724ec6d414afcd461ef31cdec48ec13881a65b6b3d0e2ed2416176f.png

Summary#

Quantum teleportation is a protocol where one party can send 1 qubit to another party using 1 entangled qubit and 2 classical bits. Importantly, no cloning is not violated.