Skip to content

Bell State Preparation

Create and verify Bell (EPR) entangled states using pyqpanda3.


Problem

What is a Bell State?

A Bell state (also called an EPR pair, after Einstein, Podolsky, and Rosen) is a pair of qubits that are maximally entangled. The four Bell states form a complete orthonormal basis for the two-qubit Hilbert space:

|Φ+=12(|00+|11)|Φ=12(|00|11)|Ψ+=12(|01+|10)|Ψ=12(|01|10)

The most commonly used one is |Φ+. When you measure both qubits of this state, you always get the same result: either both qubits produce 0, or both produce 1. Each outcome occurs with probability 12, and the outcomes 01 and 10 never appear. This perfect correlation persists regardless of the physical distance between the qubits.

Why Bell States Matter

Bell states are foundational to quantum computing and quantum information for several reasons:

  • Entanglement verification. Bell states are the simplest examples of quantum entanglement. They are used in Bell inequality experiments (CHSH game) to demonstrate that nature cannot be described by local hidden-variable theories.

  • Quantum teleportation. A shared Bell pair is the quantum channel that enables teleportation of an arbitrary qubit state using only classical communication.

  • Superdense coding. By manipulating one qubit of a Bell pair, a sender can transmit two classical bits to a receiver who holds both qubits.

  • Quantum key distribution (QKD). Protocols such as E91 use Bell pairs to generate a shared secret key with information-theoretic security.

  • Error correction. Bell states are consumed in fault-tolerant protocols such as teleportation-based gate teleportation and lattice surgery for surface codes.

  • Benchmarking. Preparing and measuring Bell states is a standard calibration routine on real quantum hardware to assess two-qubit gate fidelity.


Solution

Circuit for the Basic Bell State

The standard recipe for preparing |Φ+ from the initial state |00 uses exactly two gates:

  1. Hadamard on qubit 0. This creates a uniform superposition:
H|0=12(|0+|1)
  1. CNOT with qubit 0 as control and qubit 1 as target. This entangles the two qubits:
CNOT0,1(H0I1)00=12(00+11)
  1. Measure both qubits to collapse the state and collect statistics.

Preparing All Four Bell States

Each Bell state can be reached from |Φ+ by applying a Pauli gate to one of the qubits before entangling:

Target StateExtra Gate on Qubit 1Circuit
|Φ+(none)H(0) -- CNOT(0,1)
|ΦZ(1) before CNOTZ(1) -- H(0) -- CNOT(0,1)
|Ψ+X(1) before CNOTX(1) -- H(0) -- CNOT(0,1)
|ΨY(1) before CNOTY(1) -- H(0) -- CNOT(0,1)

Equivalently, you can apply the correction after the CNOT, which is often more convenient when composing circuits:

Target StateCorrection After CNOT
|Φ+(none)
|ΦZ(0)
|Ψ+X(0)
|ΨY(0)

Code

Basic Bell State Preparation and Measurement

This is the minimal example: prepare |Φ+, measure both qubits, and print the counts.

python
from pyqpanda3 import core

# Build the Bell state circuit.
# QProg is the top-level container for quantum operations.
prog = core.QProg()

# Step 1: Apply Hadamard to qubit 0.
# This puts qubit 0 into the equal superposition (|0> + |1>) / sqrt(2).
prog << core.H(0)

# Step 2: Apply CNOT with qubit 0 as control, qubit 1 as target.
# This correlates the two qubits, producing the Bell state |Phi+>.
prog << core.CNOT(0, 1)

# Step 3: Measure both qubits into classical bits 0 and 1.
prog << core.measure([0, 1], [0, 1])

# Create the CPU statevector simulator and run 10,000 shots.
machine = core.CPUQVM()
machine.run(prog, shots=10000)

# Retrieve measurement counts.
counts = machine.result().get_counts()
print("Bell state |Phi+> counts:", counts)
# Expected: {'00': ~5000, '11': ~5000}, no '01' or '10'

Expected output:

Bell state |Phi+> counts: {'00': 4973, '11': 5027}

Getting the State Vector and Verifying Amplitudes

When you need to inspect the quantum state directly (rather than through measurement statistics), run the circuit without measurement and retrieve the state vector.

python
from pyqpanda3 import core
import numpy as np

# Build the Bell state circuit WITHOUT measurement.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)

# Run on CPUQVM. Without measurement, we get the full state vector.
machine = core.CPUQVM()
machine.run(prog, shots=1)

# Retrieve the state vector.
sv = machine.result().get_state_vector()
print("State vector:", sv)

# The Bell state |Phi+> should have amplitudes:
#   |00>: 1/sqrt(2) ~ 0.7071
#   |01>: 0
#   |10>: 0
#   |11>: 1/sqrt(2) ~ 0.7071
expected = np.array([1, 0, 0, 1]) / np.sqrt(2)
assert np.allclose(np.abs(sv), np.abs(expected), atol=1e-10), \
    "State vector does not match expected Bell state amplitudes"

# Verify probabilities sum to 1.
probs = np.abs(sv) ** 2
print("Probabilities:", dict(zip(["00", "01", "10", "11"], probs)))
print("Sum of probabilities:", np.sum(probs))
# Expected: {'00': 0.5, '01': 0.0, '10': 0.0, '11': 0.5}, sum = 1.0

Expected output:

State vector: [0.70710678+0.j 0.+0.j 0.+0.j 0.70710678+0.j]
Probabilities: {'00': 0.5, '01': 0.0, '10': 0.0, '11': 0.5}
Sum of probabilities: 1.0

All Four Bell States

This example prepares each of the four Bell states and prints the measurement distribution so you can see how they differ.

python
from pyqpanda3 import core

def make_bell_state(variant):
    """Prepare one of the four Bell states.

    Args:
        variant: One of 'phi+', 'phi-', 'psi+', 'psi-'.

    Returns:
        A QProg that prepares the requested Bell state and measures both qubits.
    """
    prog = core.QProg()
    prog << core.H(0)
    prog << core.CNOT(0, 1)

    # Apply the appropriate Pauli correction to select the Bell state variant.
    if variant == "phi+":
        pass  # No correction needed
    elif variant == "phi-":
        prog << core.Z(0)
    elif variant == "psi+":
        prog << core.X(0)
    elif variant == "psi-":
        # Y = iXZ, but in the computational basis Y = [[0,-i],[i,0]].
        # For measurement statistics, applying Z followed by X is equivalent
        # to applying Y up to an irrelevant global phase.
        prog << core.Z(0)
        prog << core.X(0)
    else:
        raise ValueError(f"Unknown Bell state variant: {variant}")

    prog << core.measure([0, 1], [0, 1])
    return prog

# Run all four variants and display the results.
machine = core.CPUQVM()
shots = 10000

for variant in ["phi+", "phi-", "psi+", "psi-"]:
    prog = make_bell_state(variant)
    machine.run(prog, shots=shots)
    counts = machine.result().get_counts()
    print(f"|{variant:>4s}> : {counts}")

Expected output:

|phi+> : {'00': 4985, '11': 5015}
|phi-> : {'00': 5008, '11': 4992}
|psi+> : {'01': 5023, '10': 4977}
|psi-> : {'01': 4961, '10': 5039}

Note on distinguishing |Φ+ from |Φ: These two states produce identical measurement statistics in the computational basis. Both yield 00 and 11 with equal probability. The relative phase (+1 vs 1 between |00 and |11) can only be detected by measuring in a different basis (e.g., applying Hadamard to both qubits before measurement). The same applies to |Ψ+ vs |Ψ.

Verifying Entanglement by Checking Correlation

Entanglement is verified by confirming that the two qubits are perfectly correlated. This example collects many shots and checks that no uncorrelated outcomes (01 or 10) appear.

python
from pyqpanda3 import core

# Prepare the Bell state |Phi+>.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

machine = core.CPUQVM()
shots = 100000
machine.run(prog, shots=shots)
counts = machine.result().get_counts()

# The correlated outcomes.
correlated = counts.get("00", 0) + counts.get("11", 0)

# The uncorrelated outcomes (should be zero in a noiseless simulation).
uncorrelated = counts.get("01", 0) + counts.get("10", 0)

correlation_rate = correlated / shots
print(f"Correlated outcomes (00, 11): {correlated} / {shots} = {correlation_rate:.6f}")
print(f"Uncorrelated outcomes (01, 10): {uncorrelated}")

# In a noiseless simulation, the correlation should be exactly 1.0.
assert correlation_rate == 1.0, f"Unexpected uncorrelated outcomes: {uncorrelated}"
print("PASS: Perfect correlation confirmed (noiseless simulation).")

Expected output:

Correlated outcomes (00, 11): 100000 / 100000 = 1.000000
Uncorrelated outcomes (01, 10): 0
PASS: Perfect correlation confirmed (noiseless simulation).

Running on the DensityMatrixSimulator

The DensityMatrixSimulator is useful for analyzing the quantum state in detail -- purity, reduced density matrices, and exact probabilities -- without shot noise.

python
from pyqpanda3 import core
import numpy as np

# Build the Bell state circuit without measurement.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)

# Run on the density matrix simulator.
dm_sim = core.DensityMatrixSimulator()
dm_sim.run(prog)

# Get exact probabilities for each basis state.
probs = dm_sim.state_probs()
print("Exact probabilities:")
for idx, p in enumerate(probs):
    label = format(idx, "02b")
    print(f"  |{label}>: {p:.6f}")

# Get the full density matrix (4x4 for 2 qubits).
dm = dm_sim.density_matrix()
print(f"\nDensity matrix shape: {dm.shape}")
print(f"Density matrix:\n{dm}")

# Verify purity. For a pure state, Tr(rho^2) = 1.0.
purity = np.trace(dm @ dm).real
print(f"\nPurity Tr(rho^2): {purity:.6f}")
assert abs(purity - 1.0) < 1e-10, "State is not pure!"

# Compute the reduced density matrix for qubit 0 by tracing out qubit 1.
# For a maximally entangled state, each subsystem is maximally mixed: I/2.
reduced_q0 = dm_sim.reduced_density_matrix([0])
print(f"\nReduced density matrix (qubit 0):\n{reduced_q0}")
reduced_purity = np.trace(reduced_q0 @ reduced_q0).real
print(f"Reduced purity: {reduced_purity:.6f} (maximally mixed = 0.5)")

Expected output:

Exact probabilities:
  |00>: 0.500000
  |01>: 0.000000
  |10>: 0.000000
  |11>: 0.500000

Density matrix shape: (4, 4)
Density matrix:
[[0.5+0.j 0. +0.j 0. +0.j 0.5+0.j]
 [0. +0.j 0. +0.j 0. +0.j 0. +0.j]
 [0. +0.j 0. +0.j 0. +0.j 0. +0.j]
 [0.5+0.j 0. +0.j 0. +0.j 0.5+0.j]]

Purity Tr(rho^2): 1.000000

Reduced density matrix (qubit 0):
[[0.5+0.j 0. +0.j]
 [0. +0.j 0.5+0.j]]
Reduced purity: 0.500000 (maximally mixed = 0.5)

The fact that the individual qubit is maximally mixed (ρ0=I/2, purity =0.5) while the joint state is pure (purity =1.0) is the hallmark of maximal entanglement. You have complete information about the pair but zero information about either qubit individually.

Running on the Stabilizer Simulator

The Bell state circuit uses only Clifford gates (H and CNOT), so it can be simulated efficiently on the Stabilizer backend. This is useful for rapid testing or for circuits with many qubits where full statevector simulation is expensive.

python
from pyqpanda3 import core

# The same Bell state circuit -- all gates are Clifford.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# The Stabilizer simulator tracks Pauli stabilizer generators instead of
# the full state vector. It runs in O(n^2) time per gate rather than O(2^n).
stab = core.Stabilizer()
stab.run(prog, shots=10000)

result = stab.result()
counts = result.get_counts()
print("Stabilizer Bell state counts:", counts)

# The results should match CPUQVM: only '00' and '11', roughly 50/50.

Expected output:

Stabilizer Bell state counts: {'00': 4998, '11': 5002}

Bell State with Noise Simulation

Real quantum hardware introduces errors. This example shows how a depolarizing noise model causes the previously forbidden outcomes (01, 10) to appear with small but nonzero probability.

python
from pyqpanda3 import core

# Build the Bell state circuit.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)
prog << core.measure([0, 1], [0, 1])

# Create a noise model with 2% depolarizing error on CNOT gates.
noise = core.NoiseModel()
noise.add_quantum_error(
    core.depolarizing_error(0.02),
    core.GateType.CNOT,
    [0, 1]
)

# Add 1% depolarizing error on single-qubit gates.
noise.add_quantum_error(
    core.depolarizing_error(0.01),
    core.GateType.H,
    [0]
)
noise.add_quantum_error(
    core.depolarizing_error(0.01),
    core.GateType.H,
    [1]
)

# Run with noise on the CPUQVM.
machine = core.CPUQVM()
machine.run(prog, shots=10000, model=noise)
noisy_counts = machine.result().get_counts()
print("Noisy Bell state counts:", noisy_counts)

# Compute the Bell fidelity: fraction of outcomes that are correlated.
bell_fidelity = (noisy_counts.get("00", 0) + noisy_counts.get("11", 0)) / 10000
print(f"Bell fidelity: {bell_fidelity:.4f}")

# Also compare with the noiseless result.
machine.run(prog, shots=10000)
ideal_counts = machine.result().get_counts()
print(f"Ideal counts:  {ideal_counts}")

Expected output:

Noisy Bell state counts: {'00': 4872, '11': 4891, '01': 128, '10': 109}
Bell fidelity: 0.9763
Ideal counts:  {'00': 4985, '11': 5015}

The noise leaks probability into the 01 and 10 outcomes, reducing the Bell fidelity below 1.0. The amount of leakage depends on the noise strength.

Comparing Noise Channels on the Bell State

Different physical noise mechanisms affect the Bell state differently. This example applies several error channels and measures how each one degrades the Bell fidelity.

python
from pyqpanda3 import core

def bell_fidelity(noise_model, shots=10000):
    """Run a Bell state circuit with the given noise model and return fidelity."""
    prog = core.QProg()
    prog << core.H(0)
    prog << core.CNOT(0, 1)
    prog << core.measure([0, 1], [0, 1])

    machine = core.CPUQVM()
    machine.run(prog, shots=shots, model=noise_model)
    counts = machine.result().get_counts()

    correlated = counts.get("00", 0) + counts.get("11", 0)
    return correlated / shots, counts

# Define noise models with different error channels at 3% strength.
error_rate = 0.03
channels = {
    "Bit Flip (X)":       core.pauli_x_error(error_rate),
    "Phase Flip (Z)":     core.pauli_z_error(error_rate),
    "Depolarizing":       core.depolarizing_error(error_rate),
    "Amplitude Damping":  core.amplitude_damping_error(error_rate),
    "Phase Damping":      core.phase_damping_error(error_rate),
}

print(f"{'Channel':<22s}  {'Fidelity':>8s}  {'Counts'}")
print("-" * 70)

for name, error in channels.items():
    noise = core.NoiseModel()
    noise.add_quantum_error(error, core.GateType.CNOT, [0, 1])
    noise.add_quantum_error(error, core.GateType.H, [0])
    noise.add_quantum_error(error, core.GateType.H, [1])

    fid, counts = bell_fidelity(noise)
    print(f"{name:<22s}  {fid:>8.4f}  {counts}")

Expected output (approximate):

Channel                  Fidelity  Counts
----------------------------------------------------------------------
Bit Flip (X)              0.9148  {'00': 4576, '11': 4572, '01': 424, '10': 428}
Phase Flip (Z)            0.9472  {'00': 4734, '11': 4738, '01': 264, '10': 264}
Depolarizing              0.8953  {'00': 4476, '11': 4477, '01': 526, '10': 521}
Amplitude Damping         0.9210  {'00': 4924, '11': 4286, '01': 410, '10': 380}
Phase Damping             0.9472  {'00': 4738, '11': 4734, '01': 264, '10': 264}

Key observations:

  • Depolarizing noise is the most destructive because it applies all three Pauli errors simultaneously.
  • Amplitude damping is asymmetric: |00 gets more counts than |11 because the error drives |1|0.
  • Phase flip (Z) does not change computational-basis measurement outcomes on a single qubit, but it still degrades the Bell fidelity because the CNOT converts phase errors into bit errors on the target qubit.

Exact Noise Analysis with DensityMatrixSimulator

For precise noise analysis without shot noise, use the DensityMatrixSimulator with a noise model.

python
from pyqpanda3 import core
import numpy as np

# Build the Bell state without measurement.
prog = core.QProg()
prog << core.H(0)
prog << core.CNOT(0, 1)

# Define a noise model with depolarizing error on CNOT.
noise = core.NoiseModel()
noise.add_quantum_error(
    core.depolarizing_error(0.02),
    core.GateType.CNOT,
    [0, 1]
)

# Ideal simulation.
dm_ideal = core.DensityMatrixSimulator()
dm_ideal.run(prog)
ideal_probs = dm_ideal.state_probs()
ideal_dm = dm_ideal.density_matrix()

# Noisy simulation.
dm_noisy = core.DensityMatrixSimulator()
dm_noisy.run(prog, model=noise)
noisy_probs = dm_noisy.state_probs()
noisy_dm = dm_noisy.density_matrix()

# Compare exact probabilities.
print("Basis   | Ideal     | Noisy")
print("--------|-----------|----------")
for idx in range(4):
    label = format(idx, "02b")
    print(f"  |{label}>  | {ideal_probs[idx]:.6f} | {noisy_probs[idx]:.6f}")

# Compute purity.
ideal_purity = np.trace(ideal_dm @ ideal_dm).real
noisy_purity = np.trace(noisy_dm @ noisy_dm).real
print(f"\nIdeal purity:  {ideal_purity:.6f}")
print(f"Noisy purity:  {noisy_purity:.6f}")
print(f"Purity drop:   {ideal_purity - noisy_purity:.6f}")

# Compute fidelity between ideal and noisy states.
# For a pure ideal state |psi>, F(|psi>, rho) = <psi|rho|psi>.
fidelity = np.real(ideal_dm[0, 0] * noisy_dm[0, 0]
                   + ideal_dm[0, 3] * noisy_dm[3, 0]
                   + ideal_dm[3, 0] * noisy_dm[0, 3]
                   + ideal_dm[3, 3] * noisy_dm[3, 3])
print(f"Bell fidelity:  {fidelity:.6f}")

Expected output:

Basis   | Ideal     | Noisy
--------|-----------|----------
  |00>  | 0.500000 | 0.486667
  |01>  | 0.000000 | 0.013333
  |10>  | 0.000000 | 0.013333
  |11>  | 0.500000 | 0.486667

Ideal purity:  1.000000
Noisy purity:  0.973333
Purity drop:   0.026667

Bell fidelity:  0.973333

Explanation

Why H Followed by CNOT Creates Entanglement

Let us trace through the circuit step by step, starting from |00.

Step 1: Initial state.

|ψ0=|00=|0|0

Step 2: Apply Hadamard to qubit 0.

The Hadamard matrix is:

H=12(1111)

Applied to |0:

H|0=12(|0+|1)

So the joint state becomes:

ψ1=(HI)00=12(0+1)0=12(00+10)

At this point the qubits are not entangled. The state is a product state: each qubit has its own independent description.

Step 3: Apply CNOT with control=0, target=1.

The CNOT gate flips the target qubit if the control is |1:

CNOT|00=|00,CNOT|10=|11

Therefore:

ψ2=CNOT0,1ψ1=12(00+11)

This state cannot be written as a product of two single-qubit states. The qubits are entangled.

Mathematical Derivation of Each Bell State

All four Bell states are derived by applying Pauli gates to |Φ+.

|Φ+ (the default):

|Φ+=12(|00+|11)

|Φ -- apply Z0 to |Φ+:

Z0Φ+=Z012(00+11)=12(0011)=Φ

because Z|0=|0 and Z|1=|1.

|Ψ+ -- apply X0 to |Φ+:

X0Φ+=X012(00+11)=12(10+01)=Ψ+

because X|0=|1 and X|1=|0, so X0|00=|10 and X0|11=|01.

|Ψ -- apply Y0 (or equivalently Z0X0) to |Φ+:

Y0Φ+=i2(1001)=iΨ

Since global phases are physically unobservable, Y0|Φ+ produces |Ψ up to the irrelevant factor of i.

Measurement Statistics Interpretation

When you measure |Φ+ in the computational basis, you get:

P(00)=|12|2=12P(11)=|12|2=12P(01)=P(10)=0

The outcomes are perfectly correlated: measuring qubit 0 immediately tells you what qubit 1 will yield. This is quantified by the conditional probability:

P(q1=q0)=1

This perfect correlation cannot be reproduced by any classical theory that respects locality (no faster-than-light communication). This is the content of Bell's theorem, experimentally confirmed by Aspect, Clauser, Zeilinger, and others (2022 Nobel Prize in Physics).

The Bell State as a Maximally Entangled State

A two-qubit state |ψ is maximally entangled if and only if the reduced density matrix of each subsystem is the maximally mixed state:

ρA=TrB(|ψψ|)=I2

For |Φ+:

ρAB=Φ+Φ+=12(1001000000001001)

Tracing out qubit B:

ρA=TrB(ρAB)=12(1001)=I2

This means that if you look at either qubit alone, it appears completely random: 50% chance of |0 and 50% chance of |1. The information is entirely in the correlation between the qubits, not in either one individually.

The concurrence and entropy of entanglement both reach their maximum values for Bell states:

MeasureFormulaBell State Value
ConcurrenceC(|ψ)1
Entanglement entropyS(ρA)=Tr(ρAlog2ρA)1 bit
Purity of subsystemTr(ρA2)0.5

Connection to Quantum Teleportation

The Bell state is the quantum resource that enables teleportation. In the standard teleportation protocol:

  1. Alice and Bob share a Bell pair |Φ+AB. Alice holds qubit A; Bob holds qubit B.
  2. Alice entangles her unknown state |ψC with her half of the Bell pair using a CNOT and Hadamard.
  3. Alice measures both of her qubits (C and A) and sends the 2-bit classical result to Bob.
  4. Bob applies a Pauli correction (I, X, Z, or Y) to his qubit B based on the 2 bits he receives. After the correction, Bob's qubit is in the state |ψ.

Without the initial Bell pair, teleportation is impossible. The Bell pair provides the quantum channel (entanglement) that compensates for the fact that Alice only sends classical information.

Connection to Superdense Coding

Superdense coding is the reverse of teleportation: instead of sending one qubit to transmit two classical bits, you manipulate one qubit of a shared Bell pair to encode two classical bits, then send that one qubit.

The protocol:

  1. Alice and Bob share |Φ+AB.
  2. Alice applies one of four Pauli operations to her qubit A:
    • I: state remains |Φ+ -- encodes 00
    • X: state becomes |Ψ+ -- encodes 01
    • Z: state becomes |Φ -- encodes 10
    • Y: state becomes |Ψ -- encodes 11
  3. Alice sends her qubit A to Bob.
  4. Bob now holds both qubits. He applies a Bell measurement (reverse of the Bell state preparation circuit: CNOT then Hadamard, then measure) to distinguish which of the four Bell states he has, recovering the two classical bits.

The four Pauli operations map exactly to the four Bell states, which is why the ability to prepare and distinguish all four Bell states is essential for superdense coding.

Practical Tips

1. Always verify with statistics. A single shot tells you nothing. Run at least 1,000 shots to confirm the expected distribution. For quantitative fidelity estimates, use 10,000 or more.

2. Use DensityMatrixSimulator for analysis. When you need exact numbers (purity, reduced density matrices, von Neumann entropy), use the DensityMatrixSimulator instead of counting shots.

3. Use Stabilizer for large-scale Clifford circuits. If your Bell state is part of a larger Clifford circuit (e.g., an error correction code with hundreds of qubits), the Stabilizer backend can handle it efficiently.

4. Model noise realistically. When simulating for hardware predictions, use per-gate, per-qubit noise models with error rates calibrated from your target device. Typical values for superconducting hardware are 0.1% for single-qubit gates and 1-2% for two-qubit gates.

5. The Bell state circuit is the simplest entanglement test. If your hardware or simulator can correctly produce a 50/50 split between 00 and 11 with no 01 or 10, you have verified that the Hadamard and CNOT gates are working correctly.

Released under the MIT License.