Quantum Teleportation in Q#
Implement quantum teleportation in Q# using Bell state preparation and classical communication. Run on Azure Quantum simulators and understand the protocol step by step.
Circuit diagrams
What Quantum Teleportation Actually Does
Quantum teleportation transfers the state of a qubit from one location to another using a shared entangled pair and two classical bits. No quantum information travels faster than light: the classical bits must arrive before the receiver can reconstruct the state. What teleportation achieves is the destruction of the quantum state at the source and its faithful reconstruction at the destination, with no physical qubit crossing between them.
The protocol requires three qubits: the message qubit (whose state we want to send), and two entangled qubits shared between sender (Alice) and receiver (Bob). Alice holds the message qubit and her half of the entangled pair. Bob holds the other half.
It helps to be precise about what “transfer” means here. Alice starts with a qubit in state |psi> = alpha|0> + beta|1>, where alpha and beta are complex amplitudes she does not know. She cannot measure the qubit to learn alpha and beta, because measurement would collapse the state. She cannot copy the qubit, because the no-cloning theorem forbids it. What she can do is use an entangled pair and two classical bits to make Bob’s qubit take on the exact state alpha|0> + beta|1>, while her own qubit loses that state in the process.
After teleportation completes, Alice holds two qubits in definite classical states (the measurement outcomes), and Bob holds the original quantum state. The quantum information has moved from Alice to Bob without any qubit physically traveling between them.
The Mathematics: Step by Step
Let us walk through the teleportation protocol with explicit bra-ket notation. This section makes the “magic” concrete by tracking every amplitude through the circuit.
Initial state
Alice holds the message qubit in state |psi> = alpha|0> + beta|1>. She also holds an ancilla qubit initialized to |0>, and Bob holds a qubit initialized to |0>. The three-qubit system (message, ancilla, Bob) starts in state:
|initial> = (alpha|0> + beta|1>) |0> |0>
After Bell pair creation
We apply H to the ancilla qubit, then CNOT from ancilla to Bob. This entangles Alice’s ancilla with Bob’s qubit into the Bell state (|00> + |11>)/sqrt(2). The message qubit is untouched, so the full state becomes:
|step1> = (alpha|0> + beta|1>) * (|00> + |11>) / sqrt(2)
Expanding this product gives four terms:
|step1> = (1/sqrt(2)) * [ alpha|000> + alpha|011> + beta|100> + beta|111> ]
Here the three positions represent (message, ancilla, Bob) in that order.
Before Bell measurement: rewriting in the Bell basis
The key insight of teleportation is to rewrite the message and ancilla qubits in the Bell basis. The four Bell states are:
|Phi+> = (|00> + |11>) / sqrt(2)
|Phi-> = (|00> - |11>) / sqrt(2)
|Psi+> = (|01> + |10>) / sqrt(2)
|Psi-> = (|01> - |10>) / sqrt(2)
We can invert these to express the computational basis in terms of Bell states:
|00> = (|Phi+> + |Phi->) / sqrt(2)
|10> = (|Psi+> + |Psi->) / sqrt(2)
|01> = (|Psi+> - |Psi->) / sqrt(2) (not needed here but shown for completeness)
|11> = (|Phi+> - |Phi->) / sqrt(2)
Substituting into our state and grouping by Bell states on (message, ancilla), we get:
|step1> = (1/2) * [
|Phi+>(alpha|0> + beta|1>) +
|Phi->(alpha|0> - beta|1>) +
|Psi+>(alpha|1> + beta|0>) +
|Psi->(alpha|1> - beta|0>)
]
This is the critical equation. Each Bell measurement outcome on Alice’s two qubits leaves Bob’s qubit in a specific state that relates to the original |psi> by a known Pauli operation.
After Bell measurement: the four outcomes
The Bell measurement (CNOT then H on the message qubit, followed by measuring both) projects Alice’s qubits onto one of the four computational basis states, which correspond to the four Bell states. Each outcome occurs with probability 1/4, regardless of alpha and beta.
| Alice measures (m1, m2) | Corresponding Bell state | Bob’s qubit state | Relation to |psi> |
|---|---|---|---|
| (0, 0) | |Phi+> | alpha|0> + beta|1> | I|psi> (already correct) |
| (0, 1) | |Psi+> | alpha|1> + beta|0> | X|psi> |
| (1, 0) | |Phi-> | alpha|0> - beta|1> | Z|psi> |
| (1, 1) | |Psi-> | alpha|1> - beta|0> | ZX|psi> |
Here m1 is the measurement of the message qubit (after H) and m2 is the measurement of the ancilla qubit (after CNOT).
After corrections: Bob recovers |psi>
Bob applies corrections based on the two classical bits:
- If m2 = 1, apply X (bit flip) to undo the X error
- If m1 = 1, apply Z (phase flip) to undo the Z error
The order matters: for outcome (1, 1), Bob first applies X (correcting for m2), then Z (correcting for m1), giving ZX. Since X and Z anticommute (ZX = -XZ), swapping the order would give the wrong result.
After corrections, Bob’s qubit is always alpha|0> + beta|1> = |psi>. The teleportation is complete.
Setup: Q# Project Structure
Create a Q# project targeting the Azure Quantum Development Kit:
dotnet new console -lang Q# -o TeleportDemo
cd TeleportDemo
The entry point is in Program.qs. For Azure Quantum simulation, you can also use the Python host described later in this tutorial.
Q# Language Features Used in This Code
Before diving into the implementation, let us cover the Q# language features that appear in the teleportation code. If you are coming from Python, Qiskit, or Cirq, some of these concepts will be new.
The use statement: qubit allocation
In Q#, qubits are managed resources. The use statement allocates qubits, initializes them to |0>, and automatically releases them at the end of the enclosing block. You do not manually create or destroy qubits.
// Allocate a single qubit
use q = Qubit();
// q starts in |0>, use it here
// q is released when this scope ends
// Allocate an array of 5 qubits
use register = Qubit[5];
// register[0] through register[4] are all |0>
Before a qubit is released, it must be in the |0> state. If you leave a qubit in |1> or a superposition, the runtime raises an error. This prevents accidental information leaks and forces you to think about qubit cleanup.
operation vs function
Q# distinguishes between operations (which can apply quantum gates and measure qubits) and functions (which are purely classical and cannot touch quantum state).
// Operations can call quantum gates
operation ApplyHadamard(q : Qubit) : Unit {
H(q);
}
// Functions are purely classical
function AddIntegers(a : Int, b : Int) : Int {
return a + b;
}
This distinction exists so the compiler can reason about which parts of your code interact with quantum hardware and which parts are classical computation.
The Unit return type
Unit in Q# is equivalent to void in C# or None in Python. An operation that returns Unit performs its work through side effects (applying gates, printing messages) rather than returning a value.
operation DoSomething(q : Qubit) : Unit {
H(q);
// No return statement needed
}
The Adjoint functor and is Adj
The Adjoint functor generates the inverse (conjugate transpose) of an operation. If operation U maps |0> to |psi>, then Adjoint U maps |psi> back to |0>. For an operation to support Adjoint, it must be declared with is Adj.
// This operation supports the Adjoint functor
operation PrepareState(q : Qubit) : Unit is Adj {
H(q);
T(q);
}
// Call the adjoint (inverse) of PrepareState
// This applies Adjoint T then Adjoint H (reverse order, each gate inverted)
Adjoint PrepareState(q);
The compiler automatically generates the adjoint by reversing the gate sequence and replacing each gate with its inverse. You do not write the inverse manually. If an operation contains measurements or classical side effects, it cannot be marked is Adj because those are irreversible.
let and mutable bindings
Q# uses let for immutable bindings and mutable with set for variables that change.
// Immutable binding: cannot be reassigned
let x = 42;
// Mutable binding: can be updated with set
mutable count = 0;
set count += 1;
set count = count * 2;
In the teleportation code, mutable successes = 0 tracks how many runs succeed, and set successes += 1 increments it after each successful teleportation.
Q# Implementation
namespace TeleportDemo {
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Canon;
open Microsoft.Quantum.Measurement;
open Microsoft.Quantum.Diagnostics;
/// Prepares a Bell state (|00> + |11>) / sqrt(2) on two qubits.
operation PrepareBellPair(q1 : Qubit, q2 : Qubit) : Unit {
H(q1);
CNOT(q1, q2);
}
/// Performs Alice's Bell measurement and returns the two classical bits.
operation BellMeasurement(message : Qubit, aliceAncilla : Qubit) : (Result, Result) {
CNOT(message, aliceAncilla);
H(message);
let m1 = M(message);
let m2 = M(aliceAncilla);
return (m1, m2);
}
/// Applies Bob's conditional corrections based on Alice's measurement results.
operation ApplyCorrections(bob : Qubit, m1 : Result, m2 : Result) : Unit {
if m2 == One { X(bob); }
if m1 == One { Z(bob); }
}
/// Full teleportation protocol. Returns the teleported state on Bob's qubit.
operation Teleport(
prepareMessage : (Qubit => Unit),
verifyMessage : (Qubit => Unit is Adj)
) : Bool {
use message = Qubit();
use aliceAncilla = Qubit();
use bob = Qubit();
// Prepare the state to teleport
prepareMessage(message);
// Entangle Alice's ancilla with Bob's qubit
PrepareBellPair(aliceAncilla, bob);
// Alice performs Bell measurement
let (m1, m2) = BellMeasurement(message, aliceAncilla);
// Bob applies corrections
ApplyCorrections(bob, m1, m2);
// Verify Bob's qubit matches the original state
Adjoint verifyMessage(bob);
let success = MResetZ(bob) == Zero;
Reset(message);
Reset(aliceAncilla);
return success;
}
@EntryPoint()
operation RunTeleportation() : Unit {
let nRuns = 100;
mutable successes = 0;
for _ in 1..nRuns {
// Teleport the |+> state (prepared by H)
let ok = Teleport(H, H);
if ok { set successes += 1; }
}
Message($"Teleportation success rate: {successes}/{nRuns}");
// Also test teleporting |1> (prepared by X)
mutable successes2 = 0;
for _ in 1..nRuns {
let ok = Teleport(X, X);
if ok { set successes2 += 1; }
}
Message($"Teleportation of |1> success rate: {successes2}/{nRuns}");
}
}
Both tests should report 100/100 when run on the full-state simulator, because the statevector simulator is noiseless.
The Verification Trick Explained
The Teleport operation uses an elegant technique to verify that teleportation succeeded without destroying the teleported state through measurement. Let us unpack how this works.
The problem with direct measurement
If we simply measured Bob’s qubit after teleportation, we would collapse the quantum state and only get a classical bit (Zero or One). For a state like |+> = (|0> + |1>)/sqrt(2), measuring gives Zero half the time and One half the time. A single measurement cannot confirm that the qubit was in state |+> rather than some other superposition.
The reverse-prepare-then-measure pattern
Instead of measuring directly, the code applies the inverse of the preparation operation, then checks whether the qubit returns to |0>:
// Verify Bob's qubit matches the original state
Adjoint verifyMessage(bob);
let success = MResetZ(bob) == Zero;
Here is why this works. Suppose we prepare the message qubit using operation U, so the message state is U|0> = |psi>. After successful teleportation, Bob’s qubit is also in state |psi> = U|0>. Applying Adjoint U (the inverse of U) maps Bob’s qubit back:
Adjoint U * |psi> = Adjoint U * U|0> = |0>
Measuring then gives Zero with certainty. If teleportation had failed and Bob held some other state |phi> != |psi>, then Adjoint U would not map it back to |0>, and the measurement would sometimes give One.
Concrete example: teleporting |+>
When teleporting |+>, the preparation operation is H (Hadamard). The Hadamard gate is its own inverse: H * H = I. So the verification step applies H to Bob’s qubit:
H * |+> = H * H|0> = |0>
Measuring gives Zero, confirming success.
Concrete example: teleporting |1>
When teleporting |1>, the preparation operation is X (Pauli-X). The X gate is also its own inverse: X * X = I. The verification step applies X to Bob’s qubit:
X * |1> = X * X|0> = |0>
Again, measuring gives Zero.
This pattern generalizes to any preparation that can be expressed as a unitary operation with a known adjoint, which is why the Teleport operation requires verifyMessage to be declared with is Adj.
Teleporting Arbitrary States with Parameterized Preparation
The examples above teleport specific states like |+> and |1>. To teleport an arbitrary state alpha|0> + beta|1>, you can use a parameterized rotation. The Ry gate rotates a qubit by angle theta around the Y-axis:
Ry(theta)|0> = cos(theta/2)|0> + sin(theta/2)|1>
By choosing different values of theta, you can prepare any real-amplitude single-qubit state.
namespace TeleportDemo {
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Math;
/// Prepare an arbitrary state using Ry rotation.
/// The resulting state is cos(theta/2)|0> + sin(theta/2)|1>.
operation PrepareArbitraryState(qubit : Qubit, theta : Double) : Unit is Adj {
Ry(theta, qubit);
}
operation TeleportArbitraryState() : Unit {
let theta = PI() / 3.0; // Prepare cos(pi/6)|0> + sin(pi/6)|1>
let ok = Teleport(
PrepareArbitraryState(_, theta),
PrepareArbitraryState(_, theta)
);
if ok {
Message("Arbitrary state teleported successfully.");
} else {
Message("Teleportation verification failed.");
}
}
}
The Adjoint of Ry(theta, qubit) is Ry(-theta, qubit), which undoes the rotation. The compiler generates this automatically because PrepareArbitraryState is marked is Adj.
The partial application syntax PrepareArbitraryState(_, theta) creates a new operation that takes only a Qubit argument, with theta already filled in. This matches the signature (Qubit => Unit) expected by the Teleport operation.
The Four Bell Measurement Outcomes in Detail
Let us connect the mathematical derivation above to the correction logic in the Q# code. The ApplyCorrections operation contains two conditional gates:
if m2 == One { X(bob); }
if m1 == One { Z(bob); }
Here is why each correction matches its measurement outcome:
Outcome (m1=Zero, m2=Zero): Bob already holds alpha|0> + beta|1>. No correction needed. Neither if branch fires.
Outcome (m1=Zero, m2=One): Bob holds alpha|1> + beta|0>. The X gate (bit flip) swaps |0> and |1>, giving X(alpha|1> + beta|0>) = alpha|0> + beta|1>. The m2 == One branch fires, applying X.
Outcome (m1=One, m2=Zero): Bob holds alpha|0> - beta|1>. The Z gate (phase flip) negates the |1> component, giving Z(alpha|0> - beta|1>) = alpha|0> + beta|1>. The m1 == One branch fires, applying Z.
Outcome (m1=One, m2=One): Bob holds alpha|1> - beta|0>. We need to fix both the bit flip and the phase flip. The code applies X first (for m2), then Z (for m1). Working through it: X maps alpha|1> - beta|0> to alpha|0> - beta|1>, then Z maps that to alpha|0> + beta|1>. The combined operation is ZX.
Note that the order matters. ZX != XZ for general states. Specifically, ZX = -XZ (the Pauli matrices anticommute). Since global phase does not affect measurement outcomes, ZX and XZ give physically equivalent results in this specific case, but getting the conceptual correspondence right is important for understanding the protocol.
Visualizing the State with DumpMachine
DumpMachine() is a simulator-only diagnostic that prints the full quantum state of all allocated qubits. It provides information that is fundamentally unobservable on real hardware, since determining the full quantum state of a system requires an exponential number of measurements (a process called quantum state tomography). On a simulator, the statevector is stored in classical memory and can be inspected directly.
Inserting DumpMachine calls
Add DumpMachine() at key points in the teleportation protocol to see how the state evolves:
operation TeleportWithDiagnostics(
prepareMessage : (Qubit => Unit),
verifyMessage : (Qubit => Unit is Adj)
) : Bool {
use message = Qubit();
use aliceAncilla = Qubit();
use bob = Qubit();
prepareMessage(message);
Message("After preparing message:");
DumpMachine();
PrepareBellPair(aliceAncilla, bob);
Message("After Bell pair creation (3-qubit entangled state):");
DumpMachine();
let (m1, m2) = BellMeasurement(message, aliceAncilla);
Message($"After Bell measurement: m1={m1}, m2={m2}");
Message("Bob's qubit before corrections:");
DumpMachine();
ApplyCorrections(bob, m1, m2);
Message("After corrections:");
DumpMachine();
Adjoint verifyMessage(bob);
let success = MResetZ(bob) == Zero;
Reset(message);
Reset(aliceAncilla);
return success;
}
Reading the DumpMachine output
The simulator prints a table with one row per computational basis state. For a 3-qubit system, there are 8 rows (|000> through |111>). Each row shows:
- Basis state: the computational basis label, for example |010>
- Amplitude: the complex number coefficient, displayed as real and imaginary parts
- Probability: the squared magnitude of the amplitude (the chance of measuring that basis state)
- Phase: the angle of the complex amplitude in radians
For example, after preparing the message qubit in |+> and the Bell pair, the 3-qubit state is:
(1/sqrt(2))(|0> + |1>) * (1/sqrt(2))(|00> + |11>)
= (1/2)(|000> + |011> + |100> + |111>)
The DumpMachine output looks like this (Q# uses little-endian qubit ordering, so the leftmost qubit in the label corresponds to the lowest-index qubit):
Basis | Amplitude | Probability | Phase
|000> 0.5000+0.0000i 0.2500 0.0000
|011> 0.5000+0.0000i 0.2500 0.0000
|100> 0.5000+0.0000i 0.2500 0.0000
|111> 0.5000+0.0000i 0.2500 0.0000
Each of the four basis states has amplitude 0.5 and probability 0.25 (= 0.5^2), confirming the equal superposition. The other four basis states (|001>, |010>, |101>, |110>) have zero amplitude and are omitted or shown as zero.
After Alice’s Bell measurement collapses her two qubits, the DumpMachine output shows only Bob’s qubit in a definite (possibly rotated) state, with Alice’s qubits in a classical eigenstate.
Running from Python with the qsharp Package
For local simulation without an Azure account, the qsharp Python package runs Q# directly on your machine. This is the fastest way to iterate during development.
Basic usage
import qsharp
# Compile and run the Q# entry point locally
result = qsharp.eval("TeleportDemo.RunTeleportation()")
print(result)
Loading Q# source from a file
import qsharp
# Load and compile a .qs file
with open("TeleportDemo.qs", "r") as f:
qsharp.eval(f.read())
# Now call operations defined in the compiled code
result = qsharp.eval("TeleportDemo.RunTeleportation()")
print(result)
Parameterizing Q# operations from Python
You can pass values from Python into Q# expressions using string formatting. For example, to sweep over rotation angles:
import qsharp
import math
# Test teleportation for various rotation angles
for degrees in range(0, 360, 30):
theta = math.radians(degrees)
# Inline Q# that uses the Python-computed theta value
code = f"""
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Math;
use msg = Qubit();
use anc = Qubit();
use bob = Qubit();
Ry({theta}, msg);
TeleportDemo.PrepareBellPair(anc, bob);
let (m1, m2) = TeleportDemo.BellMeasurement(msg, anc);
TeleportDemo.ApplyCorrections(bob, m1, m2);
Ry({-theta}, bob);
let result = MResetZ(bob);
Reset(msg);
Reset(anc);
result == Zero
"""
success = qsharp.eval(code)
print(f"theta={degrees}deg: {'pass' if success else 'FAIL'}")
Configuring the target profile
import qsharp
# Use the unrestricted profile for full simulator capabilities
qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
# For hardware-compatible code, use the Base profile
# qsharp.init(target_profile=qsharp.TargetProfile.Base)
The Unrestricted profile allows DumpMachine, mid-circuit measurement, and classical control flow. The Base profile restricts you to operations that real hardware supports, which is useful for checking whether your code can run on a QPU.
Running on Azure Quantum Hardware
Azure Quantum connects Q# to multiple hardware providers (IonQ, Quantinuum, and others) without rewriting your Q# code. The same teleportation code runs on a trapped-ion processor or a simulator with only a change in the target specification.
Setting up the workspace
from azure.quantum import Workspace
# Connect to your Azure Quantum workspace
workspace = Workspace(
resource_id="/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Quantum/Workspaces/<ws>",
location="eastus",
)
Submitting to IonQ
import qsharp
from azure.quantum.target import IonQ
# Target the IonQ simulator (free tier, no QPU charges)
target = workspace.get_targets(name="ionq.simulator")
# Submit the job
qsharp.init(target_profile=qsharp.TargetProfile.Base)
job = target.submit(
input_data=qsharp.compile("TeleportDemo.RunTeleportation()"),
name="teleport-test",
shots=100
)
# Wait for results
result = job.get_results()
print(result)
Resource estimation
Before running on real hardware, you can estimate the resources (qubit count, gate count, execution time) your program requires:
from azure.quantum.target.microsoft import MicrosoftEstimator
estimator = MicrosoftEstimator(workspace)
job = estimator.submit(
input_data=qsharp.compile("TeleportDemo.RunTeleportation()"),
job_params={"errorBudget": 0.01}
)
result = job.get_results()
print(f"Logical qubits: {result['logicalQubit']['codeDistance']}")
print(f"T gates: {result['physicalCounts']['breakdown']['numTfactories']}")
Resource estimation helps you understand whether your algorithm fits on current or near-term hardware.
The No-Cloning Theorem and Teleportation
The no-cloning theorem states that no quantum operation can take an arbitrary unknown state |psi> and produce two copies of it. More precisely, there is no unitary U such that U(|psi>|0>) = |psi>|psi> for all |psi>. This is a fundamental consequence of the linearity of quantum mechanics.
Teleportation respects the no-cloning theorem because the original state is destroyed during the process. Here is what happens to each qubit:
-
Before teleportation: Alice’s message qubit holds |psi> = alpha|0> + beta|1>. Bob’s qubit is part of an entangled pair and does not hold |psi>.
-
During Bell measurement: Alice measures her message qubit and ancilla. The measurement collapses her message qubit into a definite state (either |0> or |1>, depending on the outcome). The quantum information in alpha and beta is gone from Alice’s side.
-
After corrections: Bob’s qubit now holds |psi>. Alice’s message qubit holds a classical eigenstate.
At no point do both Alice and Bob hold |psi> simultaneously. The information transfers; it does not duplicate.
In the Q# code, this shows up explicitly. After the Bell measurement, the message qubit is in a definite state (the M() calls collapse it). The Reset(message) at the end returns it to |0>, confirming that the original state was destroyed:
// After this line, message qubit has been measured and collapsed
let (m1, m2) = BellMeasurement(message, aliceAncilla);
// ... Bob applies corrections and gets |psi> ...
// Clean up: message qubit is already in a definite state from measurement
Reset(message);
Reset(aliceAncilla);
If you tried to skip the Bell measurement and somehow “copy” the entanglement to create two copies of |psi>, the math simply does not allow it. The linearity of quantum gates prevents any such cloning operation from existing.
Gate Teleportation
Teleportation is not limited to transferring quantum states. A closely related protocol called gate teleportation (or “teleporting a gate”) applies a quantum operation to a qubit without directly executing that operation on the target qubit.
The idea works as follows:
- Prepare a resource state by applying gate G to one half of a Bell pair, producing (I x G)(|00> + |11>)/sqrt(2).
- Use this modified Bell pair as the shared entangled resource in the teleportation protocol.
- Teleport the input qubit through this pair.
- After corrections, the output qubit holds G|psi> rather than |psi>.
Gate teleportation is central to fault-tolerant quantum computing. In error-corrected systems, most gates (H, CNOT, S) can be applied directly to logical qubits with low overhead. But the T gate (pi/8 rotation) is expensive to implement fault-tolerantly. Instead, quantum computers use magic state distillation: they prepare high-fidelity T|+> resource states offline, then use gate teleportation to “inject” the T gate into the computation. This converts one difficult gate implementation into one easier state preparation plus one teleportation.
Understanding basic state teleportation is the foundation for understanding this more advanced technique.
Teleportation as a Building Block
Quantum teleportation is not just a demonstration protocol. It appears as a core primitive in several areas of quantum computing and quantum networking.
Quantum repeaters
Quantum communication channels (like optical fibers carrying entangled photons) lose signal over distance due to photon absorption. Classical repeaters amplify signals, but you cannot amplify quantum states without measuring them (which destroys them). Quantum repeaters solve this by chaining teleportation: establish short-range entanglement over manageable distances, then use entanglement swapping (which is just teleportation) to extend the entanglement across the full distance.
Measurement-based quantum computing (MBQC)
In the measurement-based model, computation proceeds by preparing a large entangled resource state (called a cluster state) and then performing single-qubit measurements. The measurement outcomes, combined with classical feed-forward corrections, implement arbitrary quantum gates. These corrections are exactly the teleportation corrections (X and Z) from the protocol we implemented above. Understanding teleportation is the gateway to understanding MBQC.
Distributed quantum computing
Future quantum computers may consist of multiple small quantum processors connected by classical and quantum communication links. To run a two-qubit gate between qubits on different processors, you can teleport one qubit to the other processor, apply the gate locally, and (if needed) teleport the qubit back. This trades communication time for the ability to run circuits that span multiple devices.
Error correction and magic state injection
As discussed in the gate teleportation section, teleportation is the mechanism by which expensive quantum operations (like the T gate) are applied in fault-tolerant architectures. Every large-scale quantum algorithm running on an error-corrected machine will use teleportation thousands or millions of times during execution.
Common Mistakes
Forgetting to reset qubits before release
Q#‘s use statement requires that qubits be in the |0> state when they are released. If you apply gates and measurements but forget to reset, you get a runtime error.
// Wrong: qubit may not be in |0> when released
use q = Qubit();
H(q);
// end of block: runtime error if q is not |0>
// Correct: use MResetZ to measure and reset in one step
use q = Qubit();
H(q);
let result = MResetZ(q); // Measures, then resets to |0>
The MResetZ operation combines measurement and reset into a single step. For qubits that you do not need to measure, use Reset(q) to force the qubit back to |0>.
Confusing Result and Bool
Q# has separate types for quantum measurement outcomes (Result, with values Zero and One) and classical booleans (Bool, with values true and false). They are not interchangeable.
// Wrong: cannot use Result directly as a Bool
let m = M(q);
if m { ... } // Compile error
// Correct: compare Result to Zero or One
let m = M(q);
if m == One { ... } // Works
Using DumpMachine on real hardware
DumpMachine() works only on the simulator. On real quantum hardware, calling it either produces no output or raises an error, depending on the target. This makes sense physically: reading the full quantum state of a real system would require an exponential number of measurements.
Always wrap diagnostic calls in conditional compilation or accept that they are for local debugging only.
Applying corrections in the wrong order
For the outcome (m1=One, m2=One), the correct combined correction is ZX (apply X first, then Z). If you swap the order of the if statements in ApplyCorrections, you get XZ instead. Since ZX and XZ differ by a global phase factor of -1, the physical qubit state is the same (global phase is unobservable). However, getting the conceptual mapping right matters when you extend teleportation to multi-qubit protocols or gate teleportation, where the correction structure becomes more complex.
Missing is Adj on operations passed to Adjoint
If you define a preparation operation and forget the is Adj annotation, the compiler cannot auto-generate its inverse, and calling Adjoint on it fails at compile time.
// Wrong: missing is Adj
operation PrepareState(q : Qubit) : Unit {
H(q);
T(q);
}
// Adjoint PrepareState(q); // Compile error: PrepareState does not support Adjoint
// Correct: include is Adj
operation PrepareState(q : Qubit) : Unit is Adj {
H(q);
T(q);
}
Adjoint PrepareState(q); // Compiler auto-generates: Adjoint T then Adjoint H
The is Adj annotation tells the compiler that the operation consists only of reversible gates and can be mechanically inverted.
Key Points
The no-cloning theorem is not violated: Alice’s measurement destroys the original state before Bob reconstructs it. The original message qubit is reset to |0> after the M() call.
The classical channel is mandatory: without the two bits from Alice, Bob holds a maximally mixed state and cannot reconstruct the message. The quantum channel (the entangled pair) can be established in advance, but the classical channel must happen after Alice measures.
Teleportation is a primitive in quantum networks: gate teleportation, quantum repeaters, measurement-based quantum computing, and magic state distillation all build on this core protocol. Mastering the three-qubit version in this tutorial gives you the foundation for understanding all of these applications.
Was this tutorial helpful?