Quantum Teleportation with PennyLane
Implement quantum teleportation in PennyLane: creating the entangled pair, Bell measurement, classical correction, and verifying the teleported state.
Quantum teleportation transfers the exact quantum state of one qubit to another qubit at a different location. No physical qubit moves. No quantum channel carries the state directly. What travels is two classical bits and a pre-shared entangled pair.
The result is that a qubit on one side of the world can arrive, in its exact quantum state, on the other side, without the physical hardware ever moving. This makes teleportation the fundamental primitive for quantum networking, and understanding it in code is essential for anyone working with multi-qubit protocols.
What Teleportation Achieves
Suppose Alice has a qubit in state |psi> = alpha|0> + beta|1>. She does not know alpha and beta (if she measured to find out, she would collapse the state). She wants Bob to have a qubit in exactly this state.
Alice cannot simply copy the qubit: the no-cloning theorem forbids copying an unknown quantum state. She cannot read the state and send it classically: measurement destroys superposition. But if Alice and Bob share an entangled pair, she can transfer the state by sending Bob only two classical bits, provided her original qubit is destroyed in the process.
Mathematical Derivation of Teleportation
Before writing any code, let us walk through the full mathematics. Understanding each step makes the gate sequence and classical corrections feel inevitable rather than arbitrary.
The Initial State
Alice holds qubit 0 in the unknown state she wants to teleport:
|psi> = alpha|0> + beta|1>
Alice and Bob share a Bell pair on qubits 1 and 2:
|Phi+> = (|00> + |11>) / sqrt(2)
The combined 3-qubit state is the tensor product:
|psi> ⊗ |Phi+> = (alpha|0> + beta|1>) ⊗ (|00> + |11>) / sqrt(2)
Expanding this product gives four terms:
= (1/sqrt(2)) * [ alpha|0>|00> + alpha|0>|11> + beta|1>|00> + beta|1>|11> ]
= (1/sqrt(2)) * [ alpha|000> + alpha|011> + beta|100> + beta|111> ]
Here the qubit ordering is q0 q1 q2, where q0 is Alice’s data qubit, q1 is Alice’s half of the Bell pair, and q2 is Bob’s half.
After CNOT from q0 to q1
Alice applies a CNOT gate with q0 as control and q1 as target. This flips q1 whenever q0 is |1>:
CNOT|000> = |000>
CNOT|011> = |011>
CNOT|100> = |110>
CNOT|111> = |101>
The state becomes:
(1/sqrt(2)) * [ alpha|000> + alpha|011> + beta|110> + beta|101> ]
After Hadamard on q0
Alice applies a Hadamard gate to q0. Recall that H|0> = (|0> + |1>)/sqrt(2) and H|1> = (|0> - |1>)/sqrt(2). Substituting:
alpha|000> -> alpha * (|0> + |1>)/sqrt(2) * |00> = alpha/sqrt(2) * (|000> + |100>)
alpha|011> -> alpha * (|0> + |1>)/sqrt(2) * |11> = alpha/sqrt(2) * (|011> + |111>)
beta|110> -> beta * (|0> - |1>)/sqrt(2) * |10> = beta/sqrt(2) * (|010> - |110>)
beta|101> -> beta * (|0> - |1>)/sqrt(2) * |01> = beta/sqrt(2) * (|001> - |101>)
Combining all eight terms with the overall 1/sqrt(2) factor:
(1/2) * [ alpha|000> + alpha|100> + alpha|011> + alpha|111>
+ beta|010> - beta|110> + beta|001> - beta|101> ]
Grouping by Alice’s Measurement Outcomes
Alice now measures q0 and q1. There are four possible outcomes: 00, 01, 10, 11. Group the expression by the first two qubits:
|00>: (1/2) * [ alpha|0> + beta|1> ] = (1/2) * |psi>
|01>: (1/2) * [ alpha|1> + beta|0> ] = (1/2) * X|psi>
|10>: (1/2) * [ alpha|0> - beta|1> ] = (1/2) * Z|psi>
|11>: (1/2) * [ alpha|1> - beta|0> ] = (1/2) * ZX|psi>
Each outcome occurs with probability 1/4. Bob’s qubit is always related to |psi> by a known Pauli correction:
| Alice measures | Bob’s state | Correction needed |
|---|---|---|
| 00 | alpha|0> + beta|1> | None (I) |
| 01 | alpha|1> + beta|0> | X |
| 10 | alpha|0> - beta|1> | Z |
| 11 | alpha|1> - beta|0> | XZ (apply X first, then Z) |
The correction rule is: apply X if q1 measured 1, then apply Z if q0 measured 1. After correction, Bob’s qubit is in the state |psi> regardless of which outcome Alice got.
The 3-Qubit Protocol
The teleportation protocol uses three qubits:
- q0: Alice’s qubit, in the unknown state
|psi>to be teleported - q1: Alice’s half of the shared entangled pair
- q2: Bob’s half of the shared entangled pair
The steps are:
- Prepare
|psi>on q0 - Create a Bell pair between q1 and q2
- Alice performs a Bell measurement on q0 and q1
- Alice sends the two classical measurement outcomes to Bob
- Bob applies corrections to q2 based on those outcomes
After step 5, q2 is in state |psi>. q0 has been irreversibly measured and is no longer in |psi>.
PennyLane’s Mid-Circuit Measurement API
Before implementing teleportation, let us understand the PennyLane tools we need. PennyLane provides a mid-circuit measurement system that allows you to measure qubits in the middle of a circuit and use the results to control later operations. This is essential for teleportation, where Bob’s corrections depend on Alice’s measurement outcomes.
qml.measure() Returns a MeasurementValue
The function qml.measure(wire) performs a projective measurement on the specified wire and returns a MeasurementValue object. This is not a Python integer. It is a symbolic placeholder that PennyLane resolves during execution.
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def measure_demo():
qml.Hadamard(wires=0)
m = qml.measure(0)
# m is a MeasurementValue, not 0 or 1
# You cannot use it in ordinary Python if-statements
return qml.expval(qml.PauliZ(1))
result = measure_demo()
print(f"Expectation value: {result}")
print(f"Type of MeasurementValue: {type(qml.measure.__doc__)}")
qml.cond() for Conditional Operations
The function qml.cond(condition, operation) applies a quantum operation only when a mid-circuit measurement outcome satisfies the condition. The condition is a MeasurementValue (or a boolean expression involving one).
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def cond_demo():
# Put qubit 0 into |1>
qml.PauliX(wires=0)
# Measure qubit 0
m = qml.measure(0)
# Conditionally flip qubit 1 based on measurement of qubit 0
qml.cond(m, qml.PauliX)(wires=1)
return qml.probs(wires=[1])
probs = cond_demo()
print(f"Qubit 1 probabilities: {probs}")
# Output: [0. 1.] because m=1 triggers PauliX on qubit 1
You can also combine conditions with arithmetic:
@qml.qnode(dev)
def cond_combined():
qml.Hadamard(wires=0)
qml.PauliX(wires=1)
m0 = qml.measure(0)
m1 = qml.measure(1)
# Apply PauliZ only if both measurements are 1
qml.cond(m0 & m1, qml.PauliZ)(wires=1)
return qml.probs(wires=[1])
The defer_measurements Transform
Some quantum devices do not support mid-circuit measurement natively. PennyLane provides the qml.defer_measurements transform, which rewrites mid-circuit measurements as controlled operations on ancilla qubits. The circuit behaves identically, but the measurements are deferred to the end.
dev_no_mcm = qml.device("default.qubit", wires=4)
@qml.transforms.defer_measurements
@qml.qnode(dev_no_mcm)
def deferred_demo():
qml.Hadamard(wires=0)
m = qml.measure(0)
qml.cond(m, qml.PauliX)(wires=1)
return qml.probs(wires=[1])
probs = deferred_demo()
print(f"Deferred measurement result: {probs}")
# Same as native mid-circuit measurement
The default.qubit device supports native mid-circuit measurement, so you do not need defer_measurements for the examples in this tutorial. However, the transform is essential when targeting hardware backends that lack this capability.
Step 1: Prepare the State to Teleport
For verification purposes, we parameterize the state to teleport using a rotation:
import pennylane as qml
import numpy as np
# State to teleport: Ry(theta)|0> = cos(theta/2)|0> + sin(theta/2)|1>
theta = np.pi / 3 # Some arbitrary angle
The Ry(theta) gate gives us a real-amplitude single-qubit state we can verify exactly after teleportation.
Arbitrary States on the Bloch Sphere
A single RY rotation only produces real-amplitude states. To teleport a fully general single-qubit state with a complex phase, use RY(theta) followed by RZ(phi):
# General state: RZ(phi) RY(theta)|0> = cos(theta/2)|0> + e^(i*phi)*sin(theta/2)|1>
# This covers every point on the Bloch sphere
The state RZ(phi) RY(theta)|0> produces cos(theta/2)|0> + e^(i*phi)*sin(theta/2)|1>, which parameterizes the entire Bloch sphere. Here theta controls the polar angle (how far from |0> or |1>) and phi controls the azimuthal angle (the complex phase).
Four useful test states to verify teleportation works on complex-amplitude inputs:
| State | Name | theta | phi | State vector |
|---|---|---|---|---|
| |+> | Plus | pi/2 | 0 | (1, 1)/sqrt(2) |
| |-> | Minus | pi/2 | pi | (1, -1)/sqrt(2) |
| |i> | Y eigenstate +i | pi/2 | pi/2 | (1, i)/sqrt(2) |
| Random | Arbitrary | pi/5 | pi/7 | (cos(pi/10), e^(i*pi/7)*sin(pi/10)) |
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def teleport_general(theta, phi):
# Prepare general state: RZ(phi) RY(theta)|0>
qml.RY(theta, wires=0)
qml.RZ(phi, wires=0)
# Bell pair on qubits 1 and 2
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
# Bell measurement on qubits 0 and 1
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
# Classical corrections
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
def expected_general_state(theta, phi):
"""Target state vector for RZ(phi) RY(theta)|0>."""
alpha = np.cos(theta / 2)
beta = np.exp(1j * phi) * np.sin(theta / 2)
return np.array([alpha, beta])
# Test all four states
test_states = [
("Plus |+>", np.pi/2, 0),
("Minus |->", np.pi/2, np.pi),
("Y eigenstate |i>", np.pi/2, np.pi/2),
("Random state", np.pi/5, np.pi/7),
]
for name, theta, phi in test_states:
dm = teleport_general(theta, phi)
target = expected_general_state(theta, phi)
target_dm = np.outer(target, target.conj())
fidelity = np.real(np.trace(dm @ target_dm))
print(f"{name}: fidelity = {fidelity:.6f}")
# Expected output:
# Plus |+>: fidelity = 1.000000
# Minus |->: fidelity = 1.000000
# Y eigenstate |i>: fidelity = 1.000000
# Random state: fidelity = 1.000000
All four states teleport with perfect fidelity. The protocol works for arbitrary complex-amplitude states, not just real ones.
Step 2: Create the Bell Pair
The Bell pair between q1 and q2 is created with H + CNOT:
# Bell pair creation: H on q1, CNOT from q1 to q2
# Resulting state: (|00> + |11>) / sqrt(2)
Step 3: Bell Measurement
Alice applies a CNOT from q0 to q1, then H to q0, then measures both. This is the Bell basis measurement.
PennyLane Implementation
PennyLane supports mid-circuit measurements with qml.measure(). Conditional operations based on measurement outcomes use qml.cond().
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def teleportation(theta):
# Step 1: Prepare the state to teleport on qubit 0
qml.RY(theta, wires=0)
# Step 2: Create Bell pair between qubits 1 and 2
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
# Step 3: Bell measurement on qubits 0 and 1
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
# Mid-circuit measurements
m0 = qml.measure(0)
m1 = qml.measure(1)
# Step 4: Classical corrections on qubit 2
qml.cond(m1, qml.PauliX)(wires=2) # Apply X if m1 = 1
qml.cond(m0, qml.PauliZ)(wires=2) # Apply Z if m0 = 1
# Return the density matrix of qubit 2 (state() is incompatible with defer_measurements)
return qml.density_matrix(wires=[2])
dm = teleportation(np.pi / 3)
print("Density matrix of teleported qubit:", dm)
The returned statevector covers all three qubits after measurement and correction. Because qubits 0 and 1 have been measured, the post-measurement statevector will show qubit 2 (Bob’s qubit) in the teleported state.
Comparing qml.cond vs Manual Post-Selection
There are two equivalent ways to implement the classical corrections. Understanding both helps with debugging and with targeting different hardware.
Method 1: qml.cond (Recommended)
This is the standard approach and the one shown above. PennyLane handles the conditional logic internally:
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def teleport_cond(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
Method 2: Manual Post-Selection
Instead of using qml.cond, run four separate circuits, one for each measurement outcome, and apply the correction manually in classical code. This is useful for analysis and debugging because you can inspect what Bob’s state looks like for each of Alice’s outcomes individually.
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
# Build the pre-measurement circuit and return the full density matrix
@qml.qnode(dev)
def teleport_pre_correction(theta, m0_val, m1_val):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
# Simulate specific measurement outcomes via projective preparation
# Project qubit 0 onto |m0_val>
if m0_val == 1:
qml.PauliX(wires=0)
# Project qubit 1 onto |m1_val>
if m1_val == 1:
qml.PauliX(wires=1)
# Now measure to collapse
m0 = qml.measure(0)
m1 = qml.measure(1)
# Apply the known correction for this outcome
if m1_val == 1:
qml.PauliX(wires=2)
if m0_val == 1:
qml.PauliZ(wires=2)
return qml.density_matrix(wires=[2])
# The qml.cond approach handles all branches automatically.
# The manual approach lets you inspect each branch.
theta = np.pi / 3
dm_cond = teleport_cond(theta)
print("Method 1 (qml.cond) density matrix:")
print(np.round(dm_cond, 4))
# Verify they match
target = np.array([np.cos(theta/2), np.sin(theta/2)])
target_dm = np.outer(target, target.conj())
fidelity = np.real(np.trace(dm_cond @ target_dm))
print(f"Fidelity (qml.cond): {fidelity:.6f}")
Use qml.cond for production circuits and hardware execution. Use manual post-selection when you need to examine individual branches for debugging or pedagogical purposes.
Verification: Checking the Teleported State
To verify the teleportation succeeded, compare the reduced state of qubit 2 against the intended state:
import pennylane as qml
import numpy as np
from pennylane.math import reduce_dm
dev = qml.device("default.qubit", wires=3)
def expected_state(theta):
"""State that should arrive on qubit 2 after teleportation."""
alpha = np.cos(theta / 2)
beta = np.sin(theta / 2)
return np.array([alpha, beta])
@qml.qnode(dev)
def teleportation_fidelity(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
theta = np.pi / 3
dm = teleportation_fidelity(theta)
target = expected_state(theta)
target_dm = np.outer(target, target.conj())
fidelity = np.real(np.trace(dm @ target_dm))
print(f"Fidelity: {fidelity:.6f}") # Should print 1.000000
A fidelity of 1.0 confirms the teleported state is identical to the original.
Density Matrix Tracking Through the Circuit
Inspecting the density matrix at each stage of the protocol reveals how information flows through the circuit. This section uses separate QNodes to capture the state at each checkpoint.
After State Preparation
Before any entanglement, qubit 0 is in a pure state and qubits 1 and 2 are in |0>:
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def after_state_prep(theta):
qml.RY(theta, wires=0)
return qml.density_matrix(wires=[0])
theta = np.pi / 3
dm0 = after_state_prep(theta)
print("Density matrix of q0 after state prep:")
print(np.round(dm0, 4))
# [[0.75 0.433]
# [0.433 0.25 ]]
# This is a pure state: |psi><psi| with alpha=cos(pi/6), beta=sin(pi/6)
print(f"Purity (Tr[rho^2]): {np.real(np.trace(dm0 @ dm0)):.4f}")
# Purity = 1.0 confirms pure state
After Bell Pair Creation
After creating the Bell pair on q1 and q2, those two qubits are maximally entangled. The reduced density matrix of q2 alone is maximally mixed:
@qml.qnode(dev)
def after_bell_pair(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
return qml.density_matrix(wires=[2])
dm_bob_pre = after_bell_pair(theta)
print("Bob's qubit (q2) after Bell pair creation:")
print(np.round(dm_bob_pre, 4))
# [[0.5 0. ]
# [0. 0.5]]
# Maximally mixed: Bob's qubit alone carries no information yet
print(f"Purity: {np.real(np.trace(dm_bob_pre @ dm_bob_pre)):.4f}")
# Purity = 0.5, confirming maximum mixedness for a single qubit
The fact that q2 is maximally mixed tells us that Bob cannot extract any information about |psi> without Alice’s classical bits. This is why teleportation does not allow faster-than-light communication.
After Bell Measurement (Before Corrections)
After Alice performs the CNOT and Hadamard but before corrections, Bob’s qubit is still maximally mixed from his perspective. The information about |psi> is encoded in the correlations between all three qubits, not in q2 alone:
@qml.qnode(dev)
def after_bell_measurement(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
# No measurements, no corrections yet
return qml.density_matrix(wires=[2])
dm_bob_mid = after_bell_measurement(theta)
print("Bob's qubit after Bell basis rotation (before measurement/correction):")
print(np.round(dm_bob_mid, 4))
# [[0.5 0. ]
# [0. 0.5]]
# Still maximally mixed! Bob cannot distinguish this from random noise.
After Corrections
After applying the conditional X and Z corrections, Bob’s qubit is in the pure state |psi>:
@qml.qnode(dev)
def after_corrections(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
dm_bob_final = after_corrections(theta)
print("Bob's qubit after corrections:")
print(np.round(dm_bob_final, 4))
# [[0.75 0.433]
# [0.433 0.25 ]]
# Pure state matching the original |psi>
print(f"Purity: {np.real(np.trace(dm_bob_final @ dm_bob_final)):.4f}")
# Purity = 1.0 again
The density matrix at the final step matches the initial state preparation exactly. The purity returns to 1.0, confirming that the protocol restores a pure state on Bob’s qubit.
Sweeping Theta: Fidelity Over All Input States
The teleportation protocol must work for any input state, not just one specific angle. Sweeping theta from 0 to 2*pi verifies this:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
thetas = np.linspace(0, 2 * np.pi, 50)
fidelities = []
for theta in thetas:
dm = teleportation_fidelity(theta)
target = expected_state(theta)
target_dm = np.outer(target, target.conj())
f = np.real(np.trace(dm @ target_dm))
fidelities.append(f)
plt.plot(thetas, fidelities)
plt.xlabel("theta (radians)")
plt.ylabel("Fidelity")
plt.title("Teleportation Fidelity vs Input State")
plt.ylim(0, 1.1)
plt.grid(True)
plt.savefig("teleportation_fidelity.png", dpi=80)
print(f"Minimum fidelity: {min(fidelities):.6f}")
# Should print: Minimum fidelity: 1.000000
The fidelity should be exactly 1.0 for every value of theta in a noiseless simulation. In a noisy simulation or on real hardware, fidelity will drop below 1.0, and tracking it across angles helps identify whether any input states are more susceptible to errors.
Noisy Teleportation Simulation
Real quantum hardware introduces errors at every gate. PennyLane provides the default.mixed device, which supports density matrix simulation with noise channels. This lets us model how gate errors degrade teleportation fidelity.
Setting Up the Noisy Circuit
We add a qml.DepolarizingChannel(p, wires=i) after each gate. The depolarizing channel replaces the qubit state with the maximally mixed state with probability p, modeling a gate that “forgets” information with some probability.
import pennylane as qml
import numpy as np
def noisy_teleportation_circuit(theta, p):
"""Build a noisy teleportation circuit with depolarizing error rate p."""
dev_noisy = qml.device("default.mixed", wires=3)
@qml.qnode(dev_noisy)
def circuit():
# State preparation
qml.RY(theta, wires=0)
qml.DepolarizingChannel(p, wires=0)
# Bell pair
qml.Hadamard(wires=1)
qml.DepolarizingChannel(p, wires=1)
qml.CNOT(wires=[1, 2])
qml.DepolarizingChannel(p, wires=1)
qml.DepolarizingChannel(p, wires=2)
# Bell measurement gates
qml.CNOT(wires=[0, 1])
qml.DepolarizingChannel(p, wires=0)
qml.DepolarizingChannel(p, wires=1)
qml.Hadamard(wires=0)
qml.DepolarizingChannel(p, wires=0)
# Mid-circuit measurements and corrections
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
return circuit()
# Compute fidelity for a range of error rates
theta = np.pi / 3
target = np.array([np.cos(theta / 2), np.sin(theta / 2)])
target_dm = np.outer(target, target.conj())
error_rates = [0, 0.001, 0.005, 0.01, 0.02, 0.05, 0.1]
print("Depolarizing rate p | Fidelity")
print("-" * 35)
for p in error_rates:
dm = noisy_teleportation_circuit(theta, p)
fidelity = np.real(np.trace(dm @ target_dm))
print(f"p = {p:<20.3f} | {fidelity:.4f}")
Expected output (approximate values):
| Error rate p | Fidelity |
|---|---|
| 0.000 | 1.0000 |
| 0.001 | 0.9970 |
| 0.005 | 0.9850 |
| 0.010 | 0.9700 |
| 0.020 | 0.9400 |
| 0.050 | 0.8500 |
| 0.100 | 0.7000 |
Fidelity Degradation Plot
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
ps = np.linspace(0, 0.1, 30)
fidelities_noisy = []
for p in ps:
dm = noisy_teleportation_circuit(theta, float(p))
f = np.real(np.trace(dm @ target_dm))
fidelities_noisy.append(f)
plt.figure(figsize=(8, 5))
plt.plot(ps, fidelities_noisy, 'b-', linewidth=2)
plt.xlabel("Depolarizing error rate p")
plt.ylabel("Teleportation fidelity")
plt.title("Fidelity Degradation Under Depolarizing Noise")
plt.grid(True)
plt.savefig("noisy_teleportation.png", dpi=80)
print(f"Fidelity at p=0.1: {fidelities_noisy[-1]:.4f}")
Which Steps Are Most Sensitive to Noise?
The CNOT gates are the most noise-sensitive components because they act on two qubits. A depolarizing error on either qubit after a CNOT can corrupt the entanglement that the protocol relies on. The two CNOT gates (Bell pair creation and Bell measurement) together account for most of the fidelity loss. Single-qubit gates (Hadamard, Pauli corrections) contribute less because they only affect one qubit at a time.
In practice, reducing CNOT error rates has the largest impact on teleportation fidelity. This is consistent with hardware benchmarks, where two-qubit gate error rates are typically 5 to 10 times higher than single-qubit gate error rates.
Teleportation Across Multiple Shots
The density matrix approach gives exact results in simulation, but real hardware returns individual measurement outcomes. To verify teleportation statistically, we use shots=N and collect samples.
Statistical Verification
The idea is: if we teleport |+> to Bob, then Bob’s qubit should always be in |+>. If Bob applies a Hadamard to his qubit and measures, he should always get 0 (since H|+> = |0>). Meanwhile, Alice’s measurement outcomes (m0, m1) are uniformly random across all four possibilities.
import pennylane as qml
import numpy as np
dev_shots = qml.device("default.qubit", wires=3, shots=1000)
@qml.qnode(dev_shots)
def teleport_shots():
# Prepare |+> on qubit 0: RY(pi/2)|0> = |+>
qml.RY(np.pi / 2, wires=0)
# Bell pair
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
# Bell measurement
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
# Corrections
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
# Apply H to Bob's qubit to rotate |+> -> |0>
qml.Hadamard(wires=2)
return qml.sample(wires=[0, 1, 2])
samples = teleport_shots()
alice_outcomes = samples[:, 0:2]
bob_outcomes = samples[:, 2]
# Alice's outcomes should be roughly uniform
unique, counts = np.unique(alice_outcomes, axis=0, return_counts=True)
print("Alice's measurement distribution:")
for outcome, count in zip(unique, counts):
print(f" {outcome} : {count}/1000")
# Bob's outcome should always be 0 (since H|+> = |0>)
bob_ones = np.sum(bob_outcomes)
print(f"\nBob measures 1: {bob_ones}/1000 times")
print(f"Bob measures 0: {1000 - bob_ones}/1000 times")
# Expected: Bob always measures 0 in the noiseless case
Alice’s four outcomes (00, 01, 10, 11) each appear roughly 250 times out of 1000, confirming that her measurements are uniformly random. Bob’s measurement always returns 0, confirming that his qubit is deterministically in |+> after corrections.
When to Use Shots vs Density Matrix
Use the density matrix approach (qml.density_matrix()) for exact verification during development and debugging. Use the shots-based approach (qml.sample() with shots=N) when:
- Testing code that will run on real hardware (which always returns shots)
- Simulating the statistical noise that comes from finite sampling
- Validating that your circuit behaves correctly under realistic execution conditions
Entanglement Swapping with PennyLane
Entanglement swapping extends teleportation to create entanglement between parties that have never interacted directly. This is the building block for quantum repeaters and long-distance quantum networks.
The Protocol
Three parties participate: Alice, Bob (the middle node), and Charlie.
- Alice and Bob share a Bell pair on qubits 0 and 1
- Bob and Charlie share a Bell pair on qubits 2 and 3
- Bob performs a Bell measurement on his two qubits (q1 and q2)
- Bob sends his measurement results to Alice and Charlie
- Charlie applies corrections to q3
After step 5, qubits 0 (Alice) and 3 (Charlie) are entangled in a Bell state, even though they were never part of the same entangling operation.
Implementation
import pennylane as qml
import numpy as np
dev_swap = qml.device("default.qubit", wires=4)
@qml.qnode(dev_swap)
def entanglement_swapping():
# Bell pair 1: Alice (q0) and Bob (q1)
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
# Bell pair 2: Bob (q2) and Charlie (q3)
qml.Hadamard(wires=2)
qml.CNOT(wires=[2, 3])
# Bob performs Bell measurement on q1 and q2
qml.CNOT(wires=[1, 2])
qml.Hadamard(wires=1)
m1 = qml.measure(1)
m2 = qml.measure(2)
# Charlie applies corrections to q3
qml.cond(m2, qml.PauliX)(wires=3)
qml.cond(m1, qml.PauliZ)(wires=3)
# Return the joint density matrix of Alice (q0) and Charlie (q3)
return qml.density_matrix(wires=[0, 3])
dm_ac = entanglement_swapping()
print("Density matrix of Alice (q0) and Charlie (q3):")
print(np.round(dm_ac, 4))
Verifying the Bell State
The density matrix of q0 and q3 should be that of a Bell state |Phi+> = (|00> + |11>)/sqrt(2). The corresponding density matrix is:
|Phi+><Phi+| = [[0.5, 0, 0, 0.5],
[0, 0, 0, 0 ],
[0, 0, 0, 0 ],
[0.5, 0, 0, 0.5]]
The off-diagonal elements (0.5 at positions [0,3] and [3,0]) are the signature of entanglement. They indicate quantum correlations between |00> and |11> that cannot be explained by any classical probability distribution.
# Check off-diagonal elements
print(f"Off-diagonal element [0,3]: {dm_ac[0, 3]:.4f}") # Should be 0.5
print(f"Off-diagonal element [3,0]: {dm_ac[3, 0]:.4f}") # Should be 0.5
# Compute fidelity with the ideal Bell state
bell_state = np.array([1, 0, 0, 1]) / np.sqrt(2)
bell_dm = np.outer(bell_state, bell_state.conj())
fidelity = np.real(np.trace(dm_ac @ bell_dm))
print(f"Fidelity with |Phi+>: {fidelity:.6f}") # Should be 1.000000
Alice and Charlie are now maximally entangled, despite never sharing a direct quantum channel. Bob’s Bell measurement “swapped” the entanglement from the two local pairs to the remote pair.
Quantum Repeater Chain Simulation
A quantum repeater chain extends entanglement swapping across multiple nodes. This enables long-distance quantum communication where direct fiber links suffer too much photon loss.
3-Node Chain: Alice, Bob, Charlie
Consider three nodes in a line: Alice (A), Bob (B, the middle relay), and Charlie (C).
- A-B entanglement: Bell pair on q0 (Alice) and q1 (Bob-left)
- B-C entanglement: Bell pair on q2 (Bob-right) and q3 (Charlie)
- Bob performs entanglement swapping: Bell measurement on q1, q2
- Now q0 (Alice) and q3 (Charlie) are entangled
- Alice can teleport an unknown state (on q4) to Charlie using this entanglement
This requires 5 qubits: q0-q3 for the repeater chain, and q4 for the state to teleport.
import pennylane as qml
import numpy as np
dev_repeater = qml.device("default.qubit", wires=5)
@qml.qnode(dev_repeater)
def repeater_chain_teleport(theta, phi):
# Prepare the state to teleport on q4
qml.RY(theta, wires=4)
qml.RZ(phi, wires=4)
# Step 1: Bell pair between Alice (q0) and Bob-left (q1)
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
# Step 2: Bell pair between Bob-right (q2) and Charlie (q3)
qml.Hadamard(wires=2)
qml.CNOT(wires=[2, 3])
# Step 3: Entanglement swapping at Bob (Bell measurement on q1, q2)
qml.CNOT(wires=[1, 2])
qml.Hadamard(wires=1)
m1 = qml.measure(1)
m2 = qml.measure(2)
# Corrections to establish Alice-Charlie entanglement
qml.cond(m2, qml.PauliX)(wires=3)
qml.cond(m1, qml.PauliZ)(wires=3)
# Step 4: Alice teleports q4 to Charlie using the q0-q3 Bell pair
# Bell measurement on q4 (state to teleport) and q0 (Alice's half)
qml.CNOT(wires=[4, 0])
qml.Hadamard(wires=4)
m4 = qml.measure(4)
m0 = qml.measure(0)
# Charlie corrects q3
qml.cond(m0, qml.PauliX)(wires=3)
qml.cond(m4, qml.PauliZ)(wires=3)
return qml.density_matrix(wires=[3])
# Test: teleport a state through the repeater chain
theta, phi = np.pi / 5, np.pi / 7
dm_charlie = repeater_chain_teleport(theta, phi)
# Expected state
alpha = np.cos(theta / 2)
beta = np.exp(1j * phi) * np.sin(theta / 2)
target = np.array([alpha, beta])
target_dm = np.outer(target, target.conj())
fidelity = np.real(np.trace(dm_charlie @ target_dm))
print(f"Fidelity of repeater chain teleportation: {fidelity:.6f}")
# Should print 1.000000 in noiseless simulation
The state prepared on q4 arrives faithfully at q3 (Charlie), passing through Bob’s relay node. Alice and Charlie never interact directly; their quantum link exists solely because Bob performed entanglement swapping in the middle.
In a real implementation, each node would be a separate quantum processor, the Bell pairs would be distributed via optical fiber or free-space photon links, and the classical measurement results would travel over a standard network. The key advantage of quantum repeaters is that they extend the range of entanglement distribution beyond the limits of direct photon transmission, which degrades exponentially with distance in fiber.
Circuit Optimization for Hardware
PennyLane provides tools to analyze and optimize circuits before running them on hardware. Understanding the resource cost of teleportation helps with planning hardware execution.
Resource Analysis
import pennylane as qml
import numpy as np
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def teleportation_for_specs(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.density_matrix(wires=[2])
# Count the operations in the circuit
specs = qml.specs(teleportation_for_specs)(np.pi / 3)
print("Circuit specifications:")
for key, value in specs.items():
if isinstance(value, (int, float, str)):
print(f" {key}: {value}")
Gate Decomposition
When qml.cond is compiled for hardware, the conditional operations expand into controlled gates. A conditional PauliX (controlled on a classical bit) becomes a CNOT in the deferred measurement picture. A conditional PauliZ becomes a CZ gate.
The total gate count for teleportation in the deferred measurement model is:
- 1 RY gate (state preparation)
- 2 Hadamard gates (Bell pair + Bell measurement)
- 2 CNOT gates (Bell pair + Bell measurement)
- 1 CNOT gate (from deferred conditional X)
- 1 CZ gate (from deferred conditional Z)
That is 7 gates total, with 4 two-qubit gates. The circuit depth (longest path from input to output) is 6 layers when gates on different qubits run in parallel.
Hardware Connectivity
Most superconducting hardware has limited qubit connectivity (nearest-neighbor on a grid or line). Teleportation requires CNOT gates between q0-q1, q1-q2, and possibly q1-q2 again for corrections. If these three qubits are placed on a line (q0, q1, q2), all connections are nearest-neighbor and no SWAP gates are needed. This makes teleportation particularly hardware-friendly.
Teleportation Fidelity on Real Hardware via Qiskit-PennyLane
PennyLane can connect to IBM Quantum hardware through the pennylane-qiskit plugin. This lets you run the same PennyLane circuit on real superconducting qubits.
Setup
First install the plugin:
pip install pennylane-qiskit
Running on IBM Hardware
import pennylane as qml
import numpy as np
# Connect to IBM Quantum (requires an IBM Quantum account)
# Replace 'YOUR_TOKEN' with your actual IBM Quantum API token
# dev_ibm = qml.device(
# "qiskit.ibmq",
# wires=3,
# backend="ibmq_qasm_simulator",
# ibmqx_token="YOUR_TOKEN"
# )
# For demonstration, use the Qiskit Aer simulator locally
dev_ibm = qml.device("qiskit.aer", wires=3, shots=1024)
@qml.qnode(dev_ibm)
def teleport_ibm(theta):
qml.RY(theta, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
qml.CNOT(wires=[0, 1])
qml.Hadamard(wires=0)
m0 = qml.measure(0)
m1 = qml.measure(1)
qml.cond(m1, qml.PauliX)(wires=2)
qml.cond(m0, qml.PauliZ)(wires=2)
return qml.probs(wires=[2])
# Run on the backend
# theta = np.pi / 3
# probs = teleport_ibm(theta)
# print(f"Probabilities on hardware: {probs}")
# Expected for theta=pi/3: P(0) ≈ 0.75, P(1) ≈ 0.25
Simulator vs Hardware Fidelity
On the noiseless PennyLane simulator, fidelity is exactly 1.0 for all input states. On real IBM hardware, expect fidelity in the range 0.90 to 0.97 depending on the specific device and calibration state. The fidelity loss comes from:
- Gate errors: Two-qubit CNOT gates have error rates around 0.5% to 2% on current IBM hardware
- Measurement errors: Readout errors of 1% to 5% cause incorrect classical bit values, which lead to wrong corrections
- Decoherence: T1 and T2 relaxation during the circuit execution can degrade the state
- Crosstalk: Nearby qubits can interfere with each other during gate operations
Selecting qubits with the lowest error rates and shortest CNOT gate times improves fidelity. IBM’s Qiskit transpiler can automatically map your circuit to the best available qubits on a given device.
Why Teleportation Does Not Violate No-Cloning
After the protocol completes:
- q0 has been measured and collapsed. It is no longer in
|psi>. - q2 is now in
|psi>.
The state moved, it was not copied. The no-cloning theorem says you cannot duplicate an unknown quantum state. Teleportation never duplicates it: the original is destroyed by the Bell measurement before the copy appears.
Additionally, teleportation does not transmit information faster than light. Bob cannot do anything useful with q2 until he receives Alice’s two classical bits. Until those bits arrive, his qubit is in a maximally mixed state from his perspective. The classical channel is a hard speed limit.
Common Mistakes
Quantum teleportation is a short circuit, but the details matter. Here are the most frequent errors and how to avoid them.
1. Applying Corrections in the Wrong Order
The correction rule is: apply X if q1 measured 1, then apply Z if q0 measured 1. Swapping the order of X and Z seems harmless because X and Z anticommute (ZX = -XZ), and the global phase difference (-1) is unobservable for a single qubit state. However, if the teleported qubit is part of a larger entangled system, the relative phase matters. Always apply X before Z to match the standard derivation.
# Correct order
qml.cond(m1, qml.PauliX)(wires=2) # X first
qml.cond(m0, qml.PauliZ)(wires=2) # Z second
# Swapping these works for isolated qubits but can introduce
# sign errors in multi-qubit protocols
2. Confusing MeasurementValue with a Python Integer
qml.measure(0) returns a MeasurementValue, not a Python int. You cannot use it in standard Python control flow:
# WRONG: this does not work as expected
m0 = qml.measure(0)
if m0 == 1: # This comparison does not behave like an int check
qml.PauliX(wires=2)
# CORRECT: use qml.cond
m0 = qml.measure(0)
qml.cond(m0, qml.PauliX)(wires=2)
3. Creating the Bell Pair on the Wrong Qubits
The Bell pair must be between q1 and q2 (the two qubits that are NOT the state to teleport). A common mistake is creating the Bell pair between q0 and q1, which entangles the data qubit with Alice’s measurement qubit instead of keeping it separate:
# WRONG: Bell pair on q0-q1 entangles the data qubit
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
# CORRECT: Bell pair on q1-q2 keeps data qubit (q0) separate
qml.Hadamard(wires=1)
qml.CNOT(wires=[1, 2])
4. Checking Fidelity Before Applying Corrections
Before corrections, Bob’s reduced density matrix is maximally mixed (the identity matrix divided by 2). This is by design: it prevents faster-than-light signaling. If you compute fidelity at this stage, you get 0.5 for any input state, which might look like the protocol is broken. Always apply corrections before checking fidelity.
# If you see fidelity = 0.5 for all inputs, check that
# your qml.cond corrections are actually executing.
# A missing correction turns teleportation into random noise.
5. Using qml.state() with Mid-Circuit Measurements
The qml.state() return type is incompatible with circuits that use qml.measure() and qml.cond() in some configurations. Use qml.density_matrix(wires=[2]) instead to get the reduced state of Bob’s qubit. This works reliably with both native and deferred mid-circuit measurements.
6. Not Accounting for Deferred Measurement Behavior with qml.sample()
When using qml.sample() with qml.cond, the behavior depends on whether the device supports native mid-circuit measurement. On devices that use defer_measurements, the conditional operations are converted to controlled unitaries, and the “measurement” results in the sample output may not correspond to actual projective measurements at that point in the circuit. Always test your shot-based code on default.qubit (which supports native mid-circuit measurement) before moving to other backends.
Applications in Quantum Networks
Teleportation is the primitive operation for quantum networking in the same way that bit transmission is the primitive for classical networking. A quantum repeater works by teleporting quantum states across a chain of nodes, where each node shares entangled pairs with its neighbors.
Protocols under development at organizations including QuTech, AWS, and the U.S. Department of Energy’s quantum internet initiative use teleportation to:
- Distribute entanglement between remote quantum processors
- Connect quantum computers in a distributed quantum computation
- Transmit quantum keys between nodes in a quantum-secured network
PennyLane’s qml.measure() and qml.cond() model the classical feedforward operations that make these protocols work, which is why PennyLane is increasingly used to prototype quantum network algorithms before deployment on photonic hardware.
Summary
Quantum teleportation transfers an unknown quantum state using one Bell pair and two classical bits. The protocol is three qubits, four gates, two measurements, and two conditional corrections. The mathematical derivation shows that each of Alice’s four measurement outcomes leaves Bob’s qubit in a state related to the original by at most two Pauli corrections.
In PennyLane, qml.measure() handles mid-circuit measurement and qml.cond() applies the classical corrections. The default.qubit device supports native mid-circuit measurement, while the defer_measurements transform enables compatibility with devices that do not.
Density matrix tracking reveals that Bob’s qubit remains maximally mixed until corrections are applied, which is why the classical communication channel is essential and why teleportation cannot transmit information faster than light.
Under depolarizing noise, fidelity degrades primarily due to two-qubit gate errors. On real hardware, expect fidelity between 0.90 and 0.97 depending on the device.
Entanglement swapping extends teleportation to create entanglement between parties that never interacted directly, and quantum repeater chains extend this further for long-distance quantum communication. These protocols demonstrate that teleportation is not just a theoretical curiosity but the operational foundation of quantum networking.
Further Reading
- Bell State - the entangled resource that teleportation consumes
- Quantum Entanglement - why correlated pairs make teleportation possible
- Quantum Networks - how teleportation scales to distributed quantum systems
Was this tutorial helpful?