Implementing Quantum Teleportation in Qiskit
Build a working quantum teleportation circuit in Qiskit - create an entangled pair, perform a Bell measurement, apply classical corrections, and verify the teleported state.
Circuit diagrams
Quantum teleportation transfers an unknown qubit state from one location to another using a shared entangled pair and two classical bits. No quantum channel is needed between sender and receiver after the entangled pair is distributed, but the classical channel is essential: without it, the receiver cannot reconstruct the state. This is why teleportation does not allow faster-than-light communication.
The protocol was discovered by Bennett, Brassard, Crepeau, Jozsa, Peres, and Wootters in 1993 and is a cornerstone of quantum networking. Understanding it builds intuition for Bell states, measurement, and how entanglement functions as a resource.
Installation
pip install qiskit qiskit-aer matplotlib
The Three-Qubit Protocol
Teleportation uses three qubits assigned to two parties, Alice and Bob:
- Qubit 0 (Alice): the unknown state |psi⟩ = alpha|0⟩ + beta|1⟩ to be teleported
- Qubit 1 (Alice): her half of a shared Bell pair
- Qubit 2 (Bob): his half of the shared Bell pair
The steps are:
- Prepare the Bell pair on qubits 1 and 2
- Alice performs a Bell measurement on qubits 0 and 1, collapsing them and producing 2 classical bits
- Alice sends those 2 bits to Bob over a classical channel
- Bob applies conditional corrections to qubit 2, recovering |psi⟩
The state is not copied (which would violate the no-cloning theorem); after the protocol, qubit 0 is destroyed by measurement.
Mathematical Derivation of Teleportation
To see why teleportation works, we trace through the linear algebra step by step. The initial state of the three-qubit system is |psi⟩ tensor |Phi+⟩, where |psi⟩ = alpha|0⟩ + beta|1⟩ lives on qubit 0, and the Bell pair |Phi+⟩ = (|00⟩ + |11⟩)/sqrt(2) lives on qubits 1 and 2.
Step 1: Write the full three-qubit state.
|psi⟩_0 tensor |Phi+⟩_{12} = (alpha|0⟩ + beta|1⟩) tensor (|00⟩ + |11⟩)/sqrt(2)
Expanding this product gives all eight amplitude terms organized by the three qubit values:
= (1/sqrt(2)) * [ alpha|000⟩ + alpha|011⟩ + beta|100⟩ + beta|111⟩ ]
Here the qubit ordering is |q0 q1 q2⟩. We have four nonzero amplitudes out of eight possible basis states.
Step 2: Apply CNOT from qubit 0 (control) to qubit 1 (target).
CNOT flips qubit 1 when qubit 0 is |1⟩, so |100⟩ becomes |110⟩ and |111⟩ becomes |101⟩. The state becomes:
= (1/sqrt(2)) * [ alpha|000⟩ + alpha|011⟩ + beta|110⟩ + beta|101⟩ ]
Step 3: Apply Hadamard to qubit 0.
The Hadamard maps |0⟩ to (|0⟩ + |1⟩)/sqrt(2) and |1⟩ to (|0⟩ - |1⟩)/sqrt(2). Substituting:
- alpha|000⟩ becomes (alpha/sqrt(2))(|000⟩ + |100⟩)
- alpha|011⟩ becomes (alpha/sqrt(2))(|011⟩ + |111⟩)
- beta|110⟩ becomes (beta/sqrt(2))(|010⟩ - |110⟩)
- beta|101⟩ becomes (beta/sqrt(2))(|001⟩ - |101⟩)
Combining everything with the 1/sqrt(2) prefactor and grouping by the values of qubits 0 and 1 (Alice’s measurement outcomes):
= (1/2) * [ |00⟩(alpha|0⟩ + beta|1⟩) + |01⟩(alpha|1⟩ + beta|0⟩) + |10⟩(alpha|0⟩ - beta|1⟩) + |11⟩(alpha|1⟩ - beta|0⟩) ]
This is the key result. Each of the four measurement outcomes |00⟩, |01⟩, |10⟩, |11⟩ on Alice’s qubits leaves Bob’s qubit 2 in a state that differs from |psi⟩ by at most a known Pauli operation:
| Alice’s result (q0, q1) | Bob’s state on qubit 2 | Correction needed |
|---|---|---|
| 00 | alpha|0⟩ + beta|1⟩ | I (nothing) |
| 01 | alpha|1⟩ + beta|0⟩ | X (bit-flip) |
| 10 | alpha|0⟩ - beta|1⟩ | Z (phase-flip) |
| 11 | alpha|1⟩ - beta|0⟩ | XZ (bit-flip then phase-flip) |
Because each outcome is equally likely (probability 1/4 each), Alice’s measurement is completely random. The information about alpha and beta is not revealed by the classical bits alone. Only when Bob applies the corresponding Pauli correction does the original state appear on qubit 2.
The Bell Basis and Bell Measurement
The Bell states form an orthonormal basis for two qubits. They are the four maximally entangled states:
- |Phi+⟩ = (|00⟩ + |11⟩) / sqrt(2)
- |Phi-⟩ = (|00⟩ - |11⟩) / sqrt(2)
- |Psi+⟩ = (|01⟩ + |10⟩) / sqrt(2)
- |Psi-⟩ = (|01⟩ - |10⟩) / sqrt(2)
Any two-qubit state can be expressed as a superposition of these four Bell states, just as any single-qubit state can be expressed in the computational basis {|0⟩, |1⟩}.
Preparing Bell States in Qiskit
Each Bell state is obtained from a computational basis state by applying H then CNOT:
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
import numpy as np
def make_bell_state(name):
"""Prepare one of the four Bell states: Phi+, Phi-, Psi+, Psi-."""
qc = QuantumCircuit(2)
if name == "Phi+":
# |00⟩ -> H on q0 -> CNOT -> (|00⟩+|11⟩)/sqrt(2)
qc.h(0)
qc.cx(0, 1)
elif name == "Phi-":
# |10⟩ -> H on q0 -> CNOT -> (|00⟩-|11⟩)/sqrt(2)
qc.x(0)
qc.h(0)
qc.cx(0, 1)
elif name == "Psi+":
# |01⟩ -> H on q0 -> CNOT -> (|01⟩+|10⟩)/sqrt(2)
qc.x(1)
qc.h(0)
qc.cx(0, 1)
elif name == "Psi-":
# |11⟩ -> H on q0 -> CNOT -> (|01⟩-|10⟩)/sqrt(2)
qc.x(0)
qc.x(1)
qc.h(0)
qc.cx(0, 1)
return qc
for name in ["Phi+", "Phi-", "Psi+", "Psi-"]:
qc = make_bell_state(name)
sv = Statevector(qc)
print(f"|{name}⟩ = {sv}")
Why CNOT Then H Is a Bell Measurement
A Bell measurement projects two qubits onto the Bell basis. Hardware typically cannot measure directly in the Bell basis, but we can rotate from the Bell basis to the computational basis and then measure in the computational basis.
The Bell states are related to computational basis states by:
|Phi+⟩ = (H tensor I) CNOT |00⟩
So applying CNOT followed by H is the inverse transformation: it maps Bell states back to computational basis states. When we apply CNOT(q0, q1) then H(q0) and measure both qubits, we effectively perform a Bell measurement. The measurement result tells us which Bell state the two qubits were in.
This is exactly what Alice does in the teleportation protocol.
Building the Circuit
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.primitives import StatevectorEstimator
import numpy as np
# Three quantum registers, two classical bits for Alice's measurement
qr = QuantumRegister(3, name="q")
cr = ClassicalRegister(2, name="c")
qc = QuantumCircuit(qr, cr)
# --- Step 1: Prepare the state to teleport on qubit 0 ---
# We use |+⟩ = (|0⟩ + |1⟩)/sqrt(2) so we can verify it at the end
# Replace these gates with any state preparation to generalise
qc.h(qr[0]) # Creates |+⟩ on qubit 0
qc.barrier()
# --- Step 2: Create the Bell pair on qubits 1 and 2 ---
# Bell state: (|00⟩ + |11⟩)/sqrt(2)
qc.h(qr[1])
qc.cx(qr[1], qr[2])
qc.barrier(label="Bell pair ready")
# --- Step 3: Alice's Bell measurement on qubits 0 and 1 ---
# Reverse the Bell basis preparation, then measure in the Z basis
qc.cx(qr[0], qr[1])
qc.h(qr[0])
qc.barrier(label="Bell measurement")
qc.measure(qr[0], cr[0]) # Alice's first classical bit
qc.measure(qr[1], cr[1]) # Alice's second classical bit
qc.barrier(label="Classical correction")
# --- Step 4: Bob applies conditional corrections to qubit 2 ---
# If cr[1] == 1: apply X (bit-flip correction)
# If cr[0] == 1: apply Z (phase-flip correction)
# Order matters: X before Z
with qc.if_test((cr[1], 1)):
qc.x(qr[2])
with qc.if_test((cr[0], 1)):
qc.z(qr[2])
print(qc.draw(output="text", fold=80))
Verifying the Teleported State
The best verification method for a simulator is to check qubit 2 has the correct statevector after correction. We do this by running the circuit without the final measurements, using the statevector simulator.
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, partial_trace, state_fidelity
import numpy as np
# Build a version without classical control for statevector inspection
# We manually apply the corrections by fixing a known measurement outcome
# For a complete check: run once for each of the 4 possible measurement outcomes
def build_teleport_fixed(m0, m1, init_state="plus"):
"""
Build the teleportation circuit with fixed measurement outcomes m0, m1.
This lets the statevector simulator show the final state of qubit 2.
"""
qc = QuantumCircuit(3)
# Prepare the state to teleport
if init_state == "plus":
qc.h(0) # |+⟩
elif init_state == "ry":
qc.ry(np.pi / 3, 0) # arbitrary rotation
# Bell pair on qubits 1 and 2
qc.h(1)
qc.cx(1, 2)
# Alice's Bell measurement (unitary part only)
qc.cx(0, 1)
qc.h(0)
# Apply corrections based on fixed classical bits
if m1 == 1:
qc.x(2)
if m0 == 1:
qc.z(2)
return qc
from qiskit.quantum_info import Statevector, partial_trace, state_fidelity
import numpy as np
# Reference: what |+⟩ looks like as a statevector on a single qubit
ref_plus = Statevector([1 / np.sqrt(2), 1 / np.sqrt(2)])
print("Fidelities of qubit 2 with |+⟩ for each measurement outcome:")
for m0 in [0, 1]:
for m1 in [0, 1]:
circ = build_teleport_fixed(m0, m1, init_state="plus")
sv = Statevector(circ)
state = sv.data
# Post-select on the (m0, m1) measurement outcome.
# Qiskit statevector index encodes: idx = q2*4 + q1*2 + q0.
# Extract qubit 2 amplitudes for this outcome (q0=m0, q1=m1).
amp_q2_0 = state[m0 + m1 * 2 + 0 * 4] # q2=0
amp_q2_1 = state[m0 + m1 * 2 + 1 * 4] # q2=1
norm = np.sqrt(abs(amp_q2_0) ** 2 + abs(amp_q2_1) ** 2)
q2_state = Statevector([amp_q2_0 / norm, amp_q2_1 / norm])
fidelity = state_fidelity(q2_state, ref_plus)
print(f" m0={m0}, m1={m1}: fidelity = {fidelity:.6f}")
All four measurement outcomes should give fidelity 1.0 with |+⟩, confirming that Bob’s qubit always ends up in the correct state regardless of Alice’s result.
Teleporting Different States
The teleportation protocol works for any qubit state. Let us verify this by teleporting several specific states and an arbitrary parameterized state.
Teleporting |+⟩, |-⟩, and an Arbitrary State
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, state_fidelity
import numpy as np
def build_teleport_with_prep(prep_gates, m0, m1):
"""
Build teleportation circuit with arbitrary state preparation.
prep_gates is a function that applies gates to qubit 0 of the circuit.
"""
qc = QuantumCircuit(3)
prep_gates(qc) # Prepare the state to teleport on qubit 0
qc.h(1) # Bell pair
qc.cx(1, 2)
qc.cx(0, 1) # Bell measurement (unitary part)
qc.h(0)
if m1 == 1: # Corrections
qc.x(2)
if m0 == 1:
qc.z(2)
return qc
def extract_qubit2(sv_data, m0, m1):
"""Extract and normalize qubit 2 state given Alice's outcome."""
amp0 = sv_data[m0 + m1 * 2 + 0 * 4]
amp1 = sv_data[m0 + m1 * 2 + 1 * 4]
norm = np.sqrt(abs(amp0)**2 + abs(amp1)**2)
return Statevector([amp0 / norm, amp1 / norm])
# Define states to teleport
states = {
"|+⟩": {
"prep": lambda qc: qc.h(0),
"ref": Statevector([1/np.sqrt(2), 1/np.sqrt(2)])
},
"|−⟩": {
"prep": lambda qc: (qc.h(0), qc.z(0)),
"ref": Statevector([1/np.sqrt(2), -1/np.sqrt(2)])
},
"Ry(pi/3)|0⟩": {
"prep": lambda qc: qc.ry(np.pi/3, 0),
"ref": Statevector([np.cos(np.pi/6), np.sin(np.pi/6)])
},
}
for name, info in states.items():
fidelities = []
for m0 in [0, 1]:
for m1 in [0, 1]:
qc = build_teleport_with_prep(info["prep"], m0, m1)
sv = Statevector(qc)
q2 = extract_qubit2(sv.data, m0, m1)
fidelities.append(state_fidelity(q2, info["ref"]))
avg = np.mean(fidelities)
print(f"State {name}: average fidelity = {avg:.6f}")
Parameterized Sweep Over the Bloch Sphere
To thoroughly test teleportation, we sweep over many values of theta and phi. The state cos(theta/2)|0⟩ + e^(i*phi)*sin(theta/2)|1⟩ covers the full Bloch sphere.
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, state_fidelity
import numpy as np
def teleport_arbitrary(theta, phi, m0, m1):
"""Teleport cos(theta/2)|0⟩ + e^(i*phi)*sin(theta/2)|1⟩."""
qc = QuantumCircuit(3)
# Prepare the arbitrary state using U gate
qc.u(theta, phi, 0, 0) # U(theta, phi, lambda) on qubit 0
qc.h(1)
qc.cx(1, 2)
qc.cx(0, 1)
qc.h(0)
if m1 == 1:
qc.x(2)
if m0 == 1:
qc.z(2)
return qc
# Sweep theta from 0 to pi, phi from 0 to 2*pi
thetas = np.linspace(0, np.pi, 10)
phis = np.linspace(0, 2 * np.pi, 10)
all_fidelities = []
for theta in thetas:
for phi in phis:
ref = Statevector([
np.cos(theta / 2),
np.exp(1j * phi) * np.sin(theta / 2)
])
for m0 in [0, 1]:
for m1 in [0, 1]:
qc = teleport_arbitrary(theta, phi, m0, m1)
sv = Statevector(qc)
amp0 = sv.data[m0 + m1 * 2 + 0 * 4]
amp1 = sv.data[m0 + m1 * 2 + 1 * 4]
norm = np.sqrt(abs(amp0)**2 + abs(amp1)**2)
q2 = Statevector([amp0 / norm, amp1 / norm])
all_fidelities.append(state_fidelity(q2, ref))
print(f"Tested {len(all_fidelities)} (state, outcome) combinations")
print(f"Min fidelity: {min(all_fidelities):.10f}")
print(f"Max fidelity: {max(all_fidelities):.10f}")
print(f"Mean fidelity: {np.mean(all_fidelities):.10f}")
Every fidelity should be 1.0 (up to floating-point precision), confirming that teleportation is universal: it works for any single-qubit state.
Density Matrix and Fidelity Analysis
For a more rigorous verification, we use density matrices and partial traces instead of manually extracting amplitudes. The qiskit.quantum_info module provides DensityMatrix, partial_trace, and state_fidelity for this purpose.
The idea: after the Bell measurement unitary and corrections, the full 3-qubit system is in a pure state. We trace out qubits 0 and 1 (Alice’s qubits) to obtain the reduced density matrix of qubit 2 (Bob’s qubit). If teleportation works, this reduced density matrix matches the original state’s density matrix.
from qiskit import QuantumCircuit
from qiskit.quantum_info import DensityMatrix, partial_trace, state_fidelity
import numpy as np
def teleport_density_check(theta, phi, m0, m1):
"""
Build teleportation circuit, compute density matrix of qubit 2,
and compare with the target state's density matrix.
"""
qc = QuantumCircuit(3)
qc.u(theta, phi, 0, 0) # State to teleport
qc.h(1) # Bell pair
qc.cx(1, 2)
qc.cx(0, 1) # Bell measurement (unitary)
qc.h(0)
if m1 == 1:
qc.x(2)
if m0 == 1:
qc.z(2)
return qc
# Target state density matrix (single qubit)
theta, phi = np.pi / 4, np.pi / 3
target_sv = np.array([
np.cos(theta / 2),
np.exp(1j * phi) * np.sin(theta / 2)
])
target_dm = DensityMatrix(target_sv)
print(f"Target state: theta={theta:.4f}, phi={phi:.4f}")
print(f"Target density matrix:\n{target_dm.data}\n")
print("Measurement | Bob's density matrix | Fidelity")
print("-" * 70)
for m0 in [0, 1]:
for m1 in [0, 1]:
qc = teleport_density_check(theta, phi, m0, m1)
full_dm = DensityMatrix(qc)
# Trace out qubits 0 and 1 to get Bob's reduced density matrix.
# partial_trace takes the list of qubits to trace OUT.
bob_dm = partial_trace(full_dm, [0, 1])
fid = state_fidelity(bob_dm, target_dm)
diag = f"[{bob_dm.data[0,0]:.4f}, {bob_dm.data[1,1]:.4f}]"
print(f" m0={m0}, m1={m1} | diag={diag} | {fid:.6f}")
Systematic Fidelity Table Over Many States
from qiskit import QuantumCircuit
from qiskit.quantum_info import DensityMatrix, partial_trace, state_fidelity
import numpy as np
test_states = [
("theta=0 (|0⟩)", 0.0, 0.0),
("theta=pi (|1⟩)", np.pi, 0.0),
("theta=pi/2, phi=0 (|+⟩)", np.pi/2, 0.0),
("theta=pi/2, phi=pi (|-⟩)", np.pi/2, np.pi),
("theta=pi/4, phi=pi/3", np.pi/4, np.pi/3),
("theta=2pi/3, phi=5pi/6", 2*np.pi/3, 5*np.pi/6),
]
print(f"{'State':<30} | m0=0,m1=0 | m0=0,m1=1 | m0=1,m1=0 | m0=1,m1=1")
print("-" * 90)
for label, theta, phi in test_states:
target_sv = np.array([
np.cos(theta / 2),
np.exp(1j * phi) * np.sin(theta / 2)
])
target_dm = DensityMatrix(target_sv)
fids = []
for m0 in [0, 1]:
for m1 in [0, 1]:
qc = QuantumCircuit(3)
qc.u(theta, phi, 0, 0)
qc.h(1)
qc.cx(1, 2)
qc.cx(0, 1)
qc.h(0)
if m1 == 1:
qc.x(2)
if m0 == 1:
qc.z(2)
bob_dm = partial_trace(DensityMatrix(qc), [0, 1])
fids.append(state_fidelity(bob_dm, target_dm))
row = " | ".join(f" {f:.6f} " for f in fids)
print(f"{label:<30} | {row}")
All fidelities are 1.000000, confirming teleportation works perfectly in the noiseless case for every state and every measurement outcome.
Running on the Qiskit Simulator with Classical Control
To run the complete circuit with actual measurement and conditional operations:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import AerSimulator
sim = AerSimulator()
# Run 1024 shots and collect qubit-2 statistics
# We measure qubit 2 separately to check its state
qr = QuantumRegister(3, "q")
cr_alice = ClassicalRegister(2, "alice")
cr_bob = ClassicalRegister(1, "bob")
qc_full = QuantumCircuit(qr, cr_alice, cr_bob)
qc_full.h(qr[0]) # Prepare |+⟩
qc_full.h(qr[1])
qc_full.cx(qr[1], qr[2]) # Bell pair
qc_full.cx(qr[0], qr[1])
qc_full.h(qr[0])
qc_full.measure(qr[0], cr_alice[0])
qc_full.measure(qr[1], cr_alice[1])
with qc_full.if_test((cr_alice[1], 1)):
qc_full.x(qr[2])
with qc_full.if_test((cr_alice[0], 1)):
qc_full.z(qr[2])
# Rotate back to Z basis to check: |+⟩ -> H -> |0⟩ should appear 100% of time
qc_full.h(qr[2])
qc_full.measure(qr[2], cr_bob[0])
t_qc = transpile(qc_full, sim)
result = sim.run(t_qc, shots=2048).result()
counts = result.get_counts()
# Qubit 2 should always measure 0 (the |+⟩ state maps to |0⟩ after Hadamard)
bob_zero = sum(v for k, v in counts.items() if k.split(" ")[0] == "0")
print(f"Bob measures |0⟩ (correct): {bob_zero}/2048 = {bob_zero/2048:.3f}")
The Classical Communication Requirement
Alice’s 2-bit measurement result determines which correction Bob applies:
| cr[0] | cr[1] | Bob applies |
|---|---|---|
| 0 | 0 | nothing |
| 0 | 1 | X |
| 1 | 0 | Z |
| 1 | 1 | X then Z |
Without receiving those bits, Bob’s qubit is in a maximally mixed state, indistinguishable from noise. The classical channel is the speed-of-light bottleneck that prevents superluminal signalling.
Proof That Teleportation Cannot Communicate Faster Than Light
The correction table above hints at an important fact: without the classical bits, Bob learns nothing about Alice’s state. We can prove this rigorously using density matrices. Before Bob receives the classical bits and applies corrections, his reduced density matrix is always the maximally mixed state I/2, regardless of what Alice teleports.
Why This Is True
After Alice’s Bell measurement unitary (CNOT then H) but before any corrections, the 3-qubit state is:
(1/2) * [ |00⟩(alpha|0⟩ + beta|1⟩) + |01⟩(alpha|1⟩ + beta|0⟩) + |10⟩(alpha|0⟩ - beta|1⟩) + |11⟩(alpha|1⟩ - beta|0⟩) ]
Bob does not know which outcome Alice got. From his perspective, all four outcomes are equally likely. His density matrix is the mixture (average) over all four outcomes:
rho_Bob = (1/4) * [ |psi⟩⟨psi| + X|psi⟩⟨psi|X + Z|psi⟩⟨psi|Z + XZ|psi⟩⟨psi|ZX ]
The Pauli matrices {I, X, Z, XZ} form a basis that depolarizes any state to the maximally mixed state. Therefore rho_Bob = I/2 for any |psi⟩.
Verification in Qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import DensityMatrix, partial_trace
import numpy as np
def bob_without_correction(theta, phi):
"""
Build the teleportation circuit up to (but not including) corrections.
Return Bob's reduced density matrix.
"""
qc = QuantumCircuit(3)
qc.u(theta, phi, 0, 0) # Arbitrary state to teleport
qc.h(1) # Bell pair
qc.cx(1, 2)
qc.cx(0, 1) # Bell measurement (unitary part)
qc.h(0)
# No corrections applied: Bob has not received the classical bits yet
full_dm = DensityMatrix(qc)
# Trace out Alice's qubits (0 and 1) to get Bob's state
bob_dm = partial_trace(full_dm, [0, 1])
return bob_dm
# Test with several different states Alice might teleport
test_cases = [
(0, 0, "|0⟩"),
(np.pi, 0, "|1⟩"),
(np.pi/2, 0, "|+⟩"),
(np.pi/2, np.pi/2, "|i⟩"),
(np.pi/3, np.pi/5, "arbitrary"),
]
print("Alice's state | Bob's density matrix (without corrections)")
print("-" * 60)
for theta, phi, label in test_cases:
bob = bob_without_correction(theta, phi)
print(f" {label:<12} | [[{bob.data[0,0].real:.4f}, {bob.data[0,1]:.4f}],")
print(f" | [{bob.data[1,0]:.4f}, {bob.data[1,1].real:.4f}]]")
print()
Every output is [[0.5, 0], [0, 0.5]], the maximally mixed state I/2. Bob’s qubit contains zero information about alpha and beta until he receives Alice’s two classical bits. This is the rigorous proof that quantum teleportation respects special relativity.
Entanglement Swapping
Entanglement swapping extends teleportation to create entanglement between parties that have never interacted. Consider three parties: Alice, Bob, and Charlie.
- Alice and Bob share an entangled Bell pair (qubits 0 and 1).
- Bob and Charlie share a second entangled Bell pair (qubits 2 and 3).
- Alice holds qubit 0. Bob holds qubits 1 and 2. Charlie holds qubit 3.
Bob performs a Bell measurement on his two qubits (1 and 2). After the measurement and classical communication of the result, qubits 0 and 3 become entangled, even though Alice and Charlie never interacted and their qubits were never in the same location.
This is conceptually the same as teleporting entanglement itself: Bob “teleports” qubit 1’s entanglement with qubit 0 onto qubit 3. The result is that Alice and Charlie now share a Bell pair.
Entanglement Swapping Circuit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, partial_trace, state_fidelity, DensityMatrix
import numpy as np
# 4 qubits: Alice(0), Bob(1,2), Charlie(3)
qc = QuantumCircuit(4)
# Bell pair 1: Alice (q0) and Bob (q1)
qc.h(0)
qc.cx(0, 1)
# Bell pair 2: Bob (q2) and Charlie (q3)
qc.h(2)
qc.cx(2, 3)
qc.barrier(label="Two Bell pairs ready")
# Bob performs Bell measurement on his qubits (1 and 2)
qc.cx(1, 2)
qc.h(1)
qc.barrier(label="Bob's Bell measurement")
print("Entanglement swapping circuit:")
print(qc.draw(output="text"))
# Get the full statevector
sv = Statevector(qc)
# Trace out Bob's qubits (1 and 2) to see Alice-Charlie state
dm_full = DensityMatrix(sv)
dm_ac = partial_trace(dm_full, [1, 2])
print("\nAlice-Charlie density matrix (after tracing out Bob):")
print(np.round(dm_ac.data, 4))
# Check: for each of Bob's measurement outcomes, Alice and Charlie
# should be in a Bell state
print("\nVerification per measurement outcome:")
data = sv.data # 16 amplitudes for 4 qubits
for b1 in [0, 1]:
for b2 in [0, 1]:
# Extract amplitudes where q1=b1 and q2=b2
# Index = q3*8 + q2*4 + q1*2 + q0
amps = []
for q0 in [0, 1]:
for q3 in [0, 1]:
idx = q3 * 8 + b2 * 4 + b1 * 2 + q0
amps.append(data[idx])
# amps is in order: (q0=0,q3=0), (q0=0,q3=1), (q0=1,q3=0), (q0=1,q3=1)
norm = np.sqrt(sum(abs(a)**2 for a in amps))
if norm > 1e-10:
amps_norm = [a / norm for a in amps]
ac_sv = Statevector(amps_norm)
# Check against all four Bell states
bell_phi_plus = Statevector([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)])
bell_phi_minus = Statevector([1/np.sqrt(2), 0, 0, -1/np.sqrt(2)])
bell_psi_plus = Statevector([0, 1/np.sqrt(2), 1/np.sqrt(2), 0])
bell_psi_minus = Statevector([0, 1/np.sqrt(2), -1/np.sqrt(2), 0])
for bell_name, bell_sv in [
("Phi+", bell_phi_plus), ("Phi-", bell_phi_minus),
("Psi+", bell_psi_plus), ("Psi-", bell_psi_minus)
]:
fid = state_fidelity(ac_sv, bell_sv)
if fid > 0.99:
print(f" Bob measures ({b1},{b2}): "
f"Alice-Charlie in |{bell_name}⟩ (fidelity={fid:.6f})")
For each of Bob’s four measurement outcomes, Alice and Charlie end up in one of the four Bell states. Entanglement swapping is the fundamental building block for quantum repeaters and long-distance quantum networking.
Quantum Repeaters: Extending Quantum Networks
A photon carrying quantum information loses coherence after traveling roughly 100 km through optical fiber. Unlike classical signals, quantum states cannot be copied and amplified (the no-cloning theorem prevents this). Quantum repeaters solve this problem by combining entanglement swapping with entanglement purification.
How a Quantum Repeater Chain Works
Consider establishing entanglement between two nodes separated by 300 km. Direct transmission has negligible success probability at this distance. Instead, we place intermediate repeater nodes every 100 km.
Three-node repeater chain: Alice — Node B — Node C — Dave
Step 1: Establish short-distance entanglement between adjacent nodes.
- Alice and Node B create a Bell pair over 100 km (feasible probability).
- Node B and Node C create a Bell pair over 100 km.
- Node C and Dave create a Bell pair over 100 km.
Step 2: Perform entanglement swapping at intermediate nodes.
- Node B measures its two qubits (one from each pair) in the Bell basis. This swaps entanglement so Alice and Node C now share a Bell pair.
- Node C measures its two qubits in the Bell basis. This extends entanglement so Alice and Dave now share a Bell pair.
Step 3: Purify the resulting entanglement.
- Each swapping step can degrade fidelity. Entanglement purification protocols take multiple imperfect Bell pairs and distill fewer, higher-fidelity pairs. This step requires quantum memory at each node to store qubits while purification rounds proceed.
The end result is a high-fidelity Bell pair shared between Alice and Dave across 300 km, without any single photon needing to travel the full distance. This is the architecture that a future quantum internet will use to distribute entanglement globally.
Key Challenges for Quantum Repeaters
- Quantum memory: Nodes must store qubits while waiting for classical signals from other nodes. Current quantum memory coherence times are a major bottleneck.
- Entanglement purification: Real entanglement is never perfect. Purification protocols consume multiple noisy pairs to produce one cleaner pair, requiring a surplus of entanglement resources.
- Synchronization: Nodes must coordinate classical messages across the network, introducing latency.
- Error correction: Advanced repeater designs replace purification with full quantum error correction at each node, which requires many physical qubits per logical qubit.
Mid-Circuit Measurement and Real Hardware
The teleportation circuit uses classical feedforward: measuring qubits 0 and 1 mid-circuit and conditioning subsequent gates (X and Z on qubit 2) on the results. This requires dynamic circuits, a hardware capability where mid-circuit measurements and classically conditioned gates execute within a single circuit run.
Which IBM Backends Support Dynamic Circuits
Not all IBM backends support mid-circuit measurement and if_test. The backends that do support dynamic circuits include the IBM Eagle and Heron processors. You can check programmatically:
# Check if a backend supports dynamic circuits
# This example uses the IBM Quantum runtime service
# Uncomment and fill in your credentials to run on real hardware
# from qiskit_ibm_runtime import QiskitRuntimeService
# service = QiskitRuntimeService(channel="ibm_quantum")
#
# for backend in service.backends():
# config = backend.configuration()
# if hasattr(config, 'supported_features'):
# if 'qasm3' in config.supported_features:
# print(f"{backend.name}: supports dynamic circuits")
The Deferred Measurement Alternative
If the target backend does not support mid-circuit measurement, you can use the deferred measurement principle: postpone all measurements to the end of the circuit and post-select on the classical outcomes.
The approach runs four copies of the circuit (one for each correction case) without any corrections, measures all three qubits at the end, and then classically filters the results.
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
import numpy as np
sim = AerSimulator()
# Build circuit WITHOUT mid-circuit measurement or corrections
# Measure all three qubits at the end
qc_deferred = QuantumCircuit(3, 3)
qc_deferred.h(0) # Prepare |+⟩
qc_deferred.h(1)
qc_deferred.cx(1, 2) # Bell pair
qc_deferred.cx(0, 1) # Bell measurement unitary
qc_deferred.h(0)
# Before measuring qubit 2, apply basis rotation to check |+⟩
# We need to undo the correction classically, so we measure in
# the Z basis and post-select
qc_deferred.measure([0, 1, 2], [0, 1, 2])
t_qc = transpile(qc_deferred, sim)
result = sim.run(t_qc, shots=8192).result()
counts = result.get_counts()
# Post-select: for each (m0, m1) outcome, determine what correction
# would have been applied and check if qubit 2 is consistent
print("Post-selection analysis:")
print(f"{'Outcome':<10} | {'Count':<6} | {'Expected q2 for |+⟩'}")
print("-" * 50)
for bitstring, count in sorted(counts.items()):
# Qiskit bit ordering: bitstring is "q2 q1 q0" (big-endian)
q0 = int(bitstring[2])
q1 = int(bitstring[1])
q2 = int(bitstring[0])
# Without correction, q2's meaning depends on (q0, q1):
# (0,0): no correction needed -> q2 represents teleported state directly
# (0,1): X correction needed -> q2 is bit-flipped
# (1,0): Z correction needed -> Z does not affect Z-basis measurement
# (1,1): XZ correction needed -> X flips the bit, Z invisible in Z basis
# For |+⟩, in Z basis we expect 50/50 for q2
print(f" {bitstring:<8} | {count:<6} | q0={q0}, q1={q1}, q2={q2}")
# Aggregate: apply corrections classically
corrected_zero = 0
corrected_one = 0
total = 0
for bitstring, count in counts.items():
q0 = int(bitstring[2])
q1 = int(bitstring[1])
q2 = int(bitstring[0])
# Apply X correction classically if q1 == 1
if q1 == 1:
q2 = 1 - q2 # bit flip
# Z correction does not change Z-basis measurement outcomes
if q2 == 0:
corrected_zero += count
else:
corrected_one += count
total += count
print(f"\nAfter classical correction:")
print(f" q2=0: {corrected_zero}/{total} = {corrected_zero/total:.4f}")
print(f" q2=1: {corrected_one}/{total} = {corrected_one/total:.4f}")
print(f" (For |+⟩ measured in Z basis, expect 50/50)")
The deferred measurement approach works on any backend but requires more shots (since you discard no data, you just reinterpret it) and demands careful classical post-processing.
Teleportation Fidelity Under Noise
On real hardware, noise degrades teleportation fidelity. We can simulate this using Qiskit Aer’s noise models. The following code measures how fidelity decreases as the depolarizing error rate increases.
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error
import numpy as np
def build_teleport_circuit():
"""Build a complete teleportation circuit for |+⟩ with measurement."""
qr = QuantumRegister(3, "q")
cr_alice = ClassicalRegister(2, "alice")
cr_bob = ClassicalRegister(1, "bob")
qc = QuantumCircuit(qr, cr_alice, cr_bob)
qc.h(qr[0]) # Prepare |+⟩
qc.h(qr[1]) # Bell pair
qc.cx(qr[1], qr[2])
qc.cx(qr[0], qr[1]) # Bell measurement
qc.h(qr[0])
qc.measure(qr[0], cr_alice[0])
qc.measure(qr[1], cr_alice[1])
with qc.if_test((cr_alice[1], 1)):
qc.x(qr[2])
with qc.if_test((cr_alice[0], 1)):
qc.z(qr[2])
# Rotate to Z basis: |+⟩ -> H -> |0⟩
qc.h(qr[2])
qc.measure(qr[2], cr_bob[0])
return qc
# Sweep over depolarizing error rates
error_rates = [0.0, 0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2]
shots = 4096
print(f"{'Error rate':<12} | {'P(correct)':<12} | {'Fidelity estimate'}")
print("-" * 50)
for p in error_rates:
noise_model = NoiseModel()
if p > 0:
# Add depolarizing error to single-qubit and two-qubit gates
error_1q = depolarizing_error(p, 1)
error_2q = depolarizing_error(p, 2)
noise_model.add_all_qubit_quantum_error(error_1q, ['h', 'x', 'z'])
noise_model.add_all_qubit_quantum_error(error_2q, ['cx'])
sim = AerSimulator(noise_model=noise_model)
qc = build_teleport_circuit()
t_qc = transpile(qc, sim)
result = sim.run(t_qc, shots=shots).result()
counts = result.get_counts()
# Bob should measure |0⟩ (the bob register is the leftmost in the bitstring)
correct = sum(v for k, v in counts.items() if k.split(" ")[0] == "0")
p_correct = correct / shots
# Fidelity estimate: P(correct) for a Z-basis measurement of |0⟩
# relates to fidelity as F = P(correct) for a pure state
print(f" {p:<10.3f} | {p_correct:<12.4f} | {p_correct:.4f}")
As the depolarizing error rate increases, the probability of Bob measuring the correct outcome drops from 1.0 toward 0.5 (the random guessing limit). At a 5% depolarizing rate per gate, fidelity is already noticeably degraded. At 20%, the output is close to random noise.
This sensitivity to noise is one reason why fault-tolerant quantum teleportation (using encoded logical qubits) is essential for practical quantum networking.
Gate Teleportation
State teleportation transfers a quantum state. Gate teleportation transfers the application of a quantum gate. This is a distinct and powerful idea with direct applications in fault-tolerant quantum computing.
The Problem: Non-Clifford Gates Are Expensive
In fault-tolerant architectures based on stabilizer codes (such as the surface code), Clifford gates (H, S, CNOT) can be implemented transversally with low overhead. However, non-Clifford gates like the T gate (pi/8 rotation) cannot be implemented transversally. The standard solution is gate teleportation via state injection.
T-Gate Teleportation Protocol
The protocol works as follows:
-
Prepare the resource state: Create T|+⟩ = T(|0⟩ + |1⟩)/sqrt(2) = (|0⟩ + e^(i*pi/4)|1⟩)/sqrt(2). This state is prepared offline using a magic state distillation factory.
-
Perform a Bell measurement: Given the input qubit |psi⟩ and the resource state, perform a joint Bell measurement on |psi⟩ and the resource qubit.
-
Apply Clifford corrections: Based on the Bell measurement outcome, apply a Clifford correction to the output qubit. The result is T|psi⟩ on the output, up to a known Clifford operation.
The key insight: the expensive non-Clifford operation (preparing T|+⟩) happens offline, and the online circuit uses only Clifford gates and measurements.
Gate Teleportation Circuit in Qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, state_fidelity, Operator
import numpy as np
def gate_teleport_t(m0, m1):
"""
Gate teleportation circuit for the T gate.
Qubit 0: input state |psi⟩
Qubit 1: resource state T|+⟩
After Bell measurement and corrections, qubit 1 holds T|psi⟩.
"""
qc = QuantumCircuit(2)
# Prepare input state: use |0⟩ for simplicity, then verify T|0⟩ = |0⟩
# (T gate only adds phase to |1⟩ component)
# For a more interesting test, prepare |+⟩ so T|+⟩ has a nontrivial phase
qc.h(0) # Input: |+⟩
# Prepare resource state T|+⟩ on qubit 1
qc.h(1)
qc.t(1) # T|+⟩ = (|0⟩ + e^(i*pi/4)|1⟩)/sqrt(2)
# Bell measurement on qubits 0 and 1
qc.cx(0, 1)
qc.h(0)
# Corrections based on measurement outcome
# The correction for T-gate teleportation involves Clifford gates
if m1 == 1:
qc.x(1)
if m0 == 1:
qc.z(1)
qc.s(1) # Extra S correction specific to T-gate teleportation
return qc
# Reference: T|+⟩ computed directly
ref_qc = QuantumCircuit(1)
ref_qc.h(0)
ref_qc.t(0)
ref_state = Statevector(ref_qc)
print("Gate teleportation of T gate applied to |+⟩:")
print(f"Target state T|+⟩ = {ref_state}\n")
for m0 in [0, 1]:
for m1 in [0, 1]:
qc = gate_teleport_t(m0, m1)
sv = Statevector(qc)
# Extract qubit 1 state by post-selecting on qubit 0 outcome
amp0 = sv.data[m0 + 0 * 2] # q1=0, q0=m0
amp1 = sv.data[m0 + 1 * 2] # q1=1, q0=m0
norm = np.sqrt(abs(amp0)**2 + abs(amp1)**2)
q1_state = Statevector([amp0 / norm, amp1 / norm])
fid = state_fidelity(q1_state, ref_state)
print(f" m0={m0}, m1={m1}: fidelity with T|+⟩ = {fid:.6f}")
Gate teleportation is the workhorse behind non-Clifford gate implementation in every major fault-tolerant quantum computing proposal. The cost of magic state distillation (preparing high-fidelity T|+⟩ states) dominates the overhead of fault-tolerant quantum computation.
Circuit Optimization for Teleportation
Understanding the resource requirements of the teleportation circuit helps when running on real hardware where every gate adds noise.
Gate Count Analysis
The teleportation circuit consists of three phases:
- Bell pair preparation: 1 H gate + 1 CNOT = 2 gates
- Bell measurement: 1 CNOT + 1 H = 2 gates
- Classical corrections: 0, 1, or 2 Pauli gates (X and/or Z) depending on the measurement outcome
Total gate count: 4 gates (deterministic) + 0 to 2 correction gates = 4 to 6 gates.
The circuit depth (longest path from input to output) is:
- Depth of Bell pair: 2 (H, then CNOT)
- Depth of Bell measurement: 2 (CNOT, then H)
- Depth of corrections: 1 or 2 (X and Z can sometimes be combined)
Total depth: approximately 6, which is quite shallow.
Inspecting the Transpiled Circuit
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
# Build the simplest teleportation circuit (no measurement, fixed correction)
qc = QuantumCircuit(3)
qc.h(0) # State prep (not counted in teleportation itself)
qc.h(1) # Bell pair
qc.cx(1, 2)
qc.cx(0, 1) # Bell measurement
qc.h(0)
qc.x(2) # Corrections for outcome (0,1)
print("Original circuit:")
print(qc.draw(output="text"))
print(f"Gate count: {qc.count_ops()}")
print(f"Circuit depth: {qc.depth()}")
# Transpile to a basis gate set typical of IBM hardware
sim = AerSimulator()
t_qc = transpile(qc, sim, optimization_level=3)
print(f"\nTranspiled circuit depth: {t_qc.depth()}")
print(f"Transpiled gate count: {t_qc.count_ops()}")
Decomposing if_test for Hardware Submission
Some transpilation pipelines require explicit decomposition of if_test blocks. Here is the equivalent circuit using explicit classical bit checks:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
qr = QuantumRegister(3, "q")
cr = ClassicalRegister(2, "c")
qc = QuantumCircuit(qr, cr)
# State preparation
qc.h(qr[0])
# Bell pair
qc.h(qr[1])
qc.cx(qr[1], qr[2])
# Bell measurement
qc.cx(qr[0], qr[1])
qc.h(qr[0])
qc.measure(qr[0], cr[0])
qc.measure(qr[1], cr[1])
# Classical feedforward using if_test (OpenQASM 3 style)
# This is the standard way in Qiskit 1.x
with qc.if_test((cr[1], 1)):
qc.x(qr[2])
with qc.if_test((cr[0], 1)):
qc.z(qr[2])
# For backends that support OpenQASM 3 but not if_test directly,
# you can export to QASM3:
# from qiskit.qasm3 import dumps
# print(dumps(qc))
print("Circuit with classical feedforward:")
print(qc.draw(output="text", fold=80))
Common Mistakes
Building a correct teleportation circuit requires attention to several subtle details. Here are the most common pitfalls.
1. Applying Corrections in the Wrong Order
The correction is X (controlled by cr[1]) then Z (controlled by cr[0]). Getting this backward produces incorrect results for certain input states. The X correction fixes the bit flip and the Z correction fixes the phase flip. Since X and Z anticommute (XZ = -ZX), swapping their order introduces a global phase that can matter when teleportation is used as a subroutine in a larger circuit.
# CORRECT: X before Z
with qc.if_test((cr[1], 1)):
qc.x(qr[2])
with qc.if_test((cr[0], 1)):
qc.z(qr[2])
# INCORRECT: Z before X with the same bit assignments
# This would apply the wrong correction for outcome (1,1)
2. Confusing Which Classical Bit Controls Which Correction
In the circuit above, cr[0] stores the measurement of qubit 0 (after the Hadamard) and controls the Z correction. cr[1] stores the measurement of qubit 1 (after the CNOT) and controls the X correction. Swapping these assignments means you apply X when you should apply Z and vice versa. This is one of the most frequent bugs.
A good practice: name your classical registers descriptively:
cr_z = ClassicalRegister(1, "controls_Z")
cr_x = ClassicalRegister(1, "controls_X")
3. Forgetting That Qubit 0 Is Destroyed
After Alice’s Bell measurement, qubit 0 is in a definite computational basis state. The original state |psi⟩ no longer exists on qubit 0. If you try to use qubit 0 after the measurement expecting it to still hold |psi⟩, you violate the no-cloning theorem and your circuit is wrong. Teleportation moves the state; it does not copy it.
4. Using the Wrong Simulator for if_test
The statevector simulator in Qiskit does not support if_test (classical feedforward). You must use AerSimulator() which supports dynamic circuits. If you try to run a circuit with if_test on a basic statevector backend, you get an error.
# CORRECT: use AerSimulator for circuits with if_test
from qiskit_aer import AerSimulator
sim = AerSimulator()
# INCORRECT: Statevector class cannot handle mid-circuit measurement
# sv = Statevector(circuit_with_if_test) # This will fail
5. Not Resetting Qubits Between Runs
On real hardware, qubit state can leak between shots if reset is not performed. Most simulators handle this automatically, but when running on actual IBM backends, stale qubit states from a previous shot can corrupt the next shot’s results. Use explicit reset gates if your backend does not auto-reset:
qc.reset(qr[0])
qc.reset(qr[1])
qc.reset(qr[2])
# Now begin the teleportation circuit
6. Expecting Perfect Fidelity on Noisy Hardware
Even with correct corrections, real hardware introduces depolarizing noise, readout errors, and crosstalk. A correctly implemented teleportation circuit on a noisy backend will not achieve fidelity 1.0. If you measure fidelity below 1.0, that does not necessarily mean your circuit is wrong. Compare against the expected fidelity for the device’s noise level (see the noise simulation section above) before debugging the circuit logic.
Connection to Quantum Networks
Teleportation is the mechanism behind quantum repeaters: devices that extend entanglement across long distances by teleporting quantum states through a chain of intermediate nodes. This is how a future quantum internet would route quantum information without needing a direct quantum channel between every pair of nodes.
What to Try Next
- Replace the |+⟩ preparation with
qc.ry(theta, 0)and verify the fidelity holds for arbitrary theta - Implement entanglement swapping with classical corrections applied to the Alice-Charlie pair
- Add noise to the entanglement swapping circuit and measure how fidelity degrades compared to single teleportation
- Implement a two-hop quantum repeater chain using four Bell pairs and three rounds of entanglement swapping
- Explore gate teleportation with gates other than T (for example, the Toffoli gate using two T-gate injections)
- See the Qiskit reference for
qiskit.quantum_info.partial_traceand density matrix tools - Read about surface codes to understand how fault-tolerant teleportation protects against noise
Was this tutorial helpful?