3

I'm currently experimenting with Qiskit and I wanted to implement the Ekert's E91 protocol. I had no problem writing the code, but then I got to the point of calculating the $S$ value to detect the presence of an eavesdropper. In all the papers about the protocol I have seen, it is mentioned that a value of $S$ equals to $2\sqrt{2}$ implies fully entangled states, so no intrusion occurred. On the other hand, $S \leq 2$ means that local realism was introduced by the eavesdropper.

This is the code I've come up with:

import numpy as np
from numpy import pi
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile
from qiskit_aer import AerSimulator

WITH_EVE = True A = 0 B = 1 E = 2

def main(): backend = AerSimulator() n = 100 # Desired number of bits for the key delta = 2 # Redundancy factor b = (4 + delta) * n # Total number of states to be prepared circuits = [None] * b dim = 3 if WITH_EVE else 2 # Number of qubits per circuit

angles = {
    A: [0, pi / 4, pi / 2],
    B: [pi / 4, pi / 2, 3 * pi / 4],
    E: [0, pi / 4, pi / 2],
}

bases = {
    A: np.random.randint(3, size=b),
    B: np.random.randint(3, size=b),
    E: np.random.randint(3, size=b),
}

keys = {A: "", B: "", E: ""}

measured_bits = {
    A: np.zeros(b, dtype=int),
    B: np.zeros(b, dtype=int),
    E: np.zeros(b, dtype=int),
}

# Preparation of states
for i in range(b):
    qr = QuantumRegister(dim, "qr")
    cr = ClassicalRegister(dim, "cr")
    circuits[i] = QuantumCircuit(qr, cr)

    circuits[i].h(qr[A])
    circuits[i].cx(qr[A], qr[B])
    if WITH_EVE:
        circuits[i].cx(qr[B], qr[E])

    circuits[i].ry(angles[B][bases[B][i]], B)
    circuits[i].ry(angles[A][bases[A][i]], A)
    if WITH_EVE:
        circuits[i].ry(angles[E][bases[E][i]], E)

    circuits[i].measure(list(range(dim)), list(range(dim)))

# Simulation
transpiled = transpile(circuits, backend)
results = backend.run(transpiled).result().get_counts()

for i in range(b):
    measured_states = max(results[i], key=results[i].get)[::-1]

    measured_bits[A][i] = int(measured_states[A])
    measured_bits[B][i] = int(measured_states[B])
    if WITH_EVE:
        measured_bits[E][i] = int(measured_states[E])

i = j = 0

# Key building
while i < b and j < n:
    if angles[A][bases[A][i]] == angles[B][bases[B][i]]:
        keys[A] += str(measured_bits[A][i])
        keys[B] += str(measured_bits[B][i])
        if WITH_EVE:
            keys[E] += str(measured_bits[E][i])

        j += 1
    i += 1

if j < n:
    exit(1)

print(f"\nAlice's key:\t {keys[A]}")
print(f"Bob's key:\t {keys[B]}")
if WITH_EVE:
    print(f"Eve's key:\t {keys[E]}")

# CHSH calculation
values = {(a, b): 0 for a in range(3) for b in range(3)}
counts = {(a, b): 0 for a in range(3) for b in range(3)}

for i in range(b):
    basis_a = bases[A][i]
    basis_b = bases[B][i]

    alice_measure = -1 + 2 * measured_bits[A][i]
    bob_measure = -1 + 2 * measured_bits[B][i]

    values[(basis_a, basis_b)] += alice_measure * bob_measure
    counts[(basis_a, basis_b)] += 1

for pair in values:
    if counts[pair] > 0:
        values[pair] /= counts[pair]

S = abs(values[(0, 0)] - values[(0, 2)] + values[(2, 0)] + values[(2, 2)])

print(f"\nCHSH value (S):\t {S:.3f}")

if name == "main": main()

Please note that this is a reduced version of a more well-structured program. When I run the code, I notice two inconsistencies:

  1. If you set WITH_EVE = False, $S$ will always be equals to $4$. This is not as expected, but here it is said:

    For comparison, in the classical case (or local realistic case) the upper bound is $2$, whereas if any arbitrary assignment of $+1, -1$ is allowed, it is $4$.

    Why does the first instance of $S$ reaches $4$? My main goal was to implement the original protocol, so I would want to get $2\sqrt{2}$. Is there any way to achieve that?

  2. If you set WITH_EVE = True, $S$ will mostly be below the predicted bound ($2$) but will often be higher than that.

    This is my main concern. Why does the value have this variability? Am I doing something wrong? I also tried increasing $n$, but I think anything above $50$ should do job, statistically speaking.

Thanks in advance to anyone who will help me out.

Lorenzo
  • 31
  • 3

1 Answers1

0

There are effectively two main issues that explain why your CHSH value sometimes reaches 4 (in the case without Eve) or goes below/above the expected value (in the case with Eve):

  1. You are not implementing the ‘classic’ CHSH measurement settings
    In the standard CHSH setup, one (entangled) Bell pair is used, and Alice has two possible measurement bases $\{A_0, A_1\}$, while Bob has two possible measurement bases $\{B_0, B_1\}$. Concretely, for a maximal quantum mechanical CHSH violation (Tsirelson bound $(2\sqrt{2})$, the angles typically chosen (for example) are:

    • $A_0 = 0$, $A_1 = \pi/4$
    • $B_0 = \pi/8$, $B_1 = -\pi/8$

    In your code, however, you choose the bases (looking at the angles array) from the sets

    and then completely randomly $((\mathrm{randint}(3)))$. This is not the well‐known CHSH protocol setup, where typically only two angles per party are needed, and certainly not the pair of ‘optimal’ angles that yield $2\sqrt{2}$.

    • If Alice and Bob happen by chance to choose the same angle (or angles that produce perfect correlation for the Bell or GHZ state you prepared), the result in the formula for $S$ can be very strongly correlated. In the extreme, this can lead to $|S|=4$ (this is the mathematically maximum value, if all measurements “perfectly” reinforce each other).
    • In other runs, especially when a third qubit (Eve) is also involved, that extra measurement disturbs the total quantum superposition, leading to strongly varying outcomes for $S$. This can easily fall below the classical limit of 2 or slightly above it, depending on chance and the randomly assigned basis label.
  2. You are using (with ‘WITH_EVE=False’) a non‐standard state and measurement sequence
    The code does create a Bell‐ or GHZ‐like state:

    circuits[i].h(qr[A])
    circuits[i].cx(qr[A], qr[B])
    if WITH_EVE:
        circuits[i].cx(qr[B], qr[E])
    

    But immediately afterward, you apply $\mathrm{RY}(\theta)$ rotations to all qubits (including Alice and Bob) and then measure in the computational basis. For a standard CHSH test, you would normally want:

    • Exactly two measurement settings for Alice (A0, A1) and two for Bob (B0, B1),
    • A singlet/fully entangled state (e.g., $\tfrac{1}{\sqrt{2}}(\lvert 01\rangle - \lvert 10\rangle)$),
    • Randomly choose whether you use $\{A_0\}$ or $\{A_1\}$ (and for Bob $\{B_0\}$ or $\{B_1\}$) on each measurement,
    • Then compute the correlations specifically for the four combinations $(A_0, B_0)$, $(A_0, B_1)$, $(A_1, B_0)$, $(A_1, B_1)$.

    In your code, for each ‘shot’ the angles $\theta$ and the corresponding basis label $\{0,1,2\}$ are chosen completely randomly, and then you derive a CHSH‐type correlation from that. This does not produce a standard CHSH measurement, but rather a mix of many angles. Hence you get a very erratic result, which in some cases can go up to 4 (because you happened to choose angles that perfectly correlate the measurements), or in the presence of Eve can go off in any direction.

How can you get $2\sqrt{2}$?

  1. Create a pure Bell state
    The simplest is $\frac{1}{\sqrt{2}}(\lvert 00\rangle + \lvert 11\rangle)$ or the singlet $\frac{1}{\sqrt{2}}(\lvert 01\rangle - \lvert 10\rangle)$. In Qiskit, for example, you can do:

    qc.h(qr[A])
    qc.cx(qr[A], qr[B])
    

    to create $\frac{1}{\sqrt{2}}(\lvert 00\rangle + \lvert 11\rangle)$ (if both qubits start in $\lvert 0\rangle$).

  2. Apply the CHSH basis rotations
    Instead of three possible $\mathrm{RY}$ angles, pick for each measurement on Alice either $\theta = 0$ (or $\pi/2$, depending on the convention) or $\theta = \pi/4$. For Bob, pick either $\theta = \pi/8$ or $\theta = -\pi/8$.

Why do you often get < 2 (or sometimes just above) with Eve?

As soon as an additional party (Eve) measures the same entangled qubits, the correlations between Alice and Bob become disturbed. In a genuine CHSH setup, there are only two parties. If Eve also carries out (partial) measurements, the quantum state of the system changes (collapse, decoherence). As a result, the nice two‐party correlation disappears or is disturbed in unpredictable ways. Hence, by choosing random angles and having a third qubit measured, you get strong fluctuations in $S$.

With the modified code S reached 2.47, so not the maximum value, could be due to noise.

import numpy as np
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import AerSimulator

First we define the angles:

Alice: A0 = 0, A1 = pi/4

Bob: B0 = pi/8, B1 = -pi/8

A0 = 0.0 A1 = np.pi/4 B0 = np.pi/8 B1 = -np.pi/8

Eve chooses to measure Bob's qubit in one of Bob's two angles:

This means that Eve uses the same measurement angles as Bob

E0 = B0 E1 = B1

(Optionally, you can force Eve to use only one angle or random. Here we choose random.)

If EVE_ACTIVE = True, then Eve is active.

EVE_ACTIVE = False

We perform 1000 runs and gather data

N_RUNS = 1000

Measurement of qubit output maps |0> -> +1, |1> -> -1 in correlation calculations

def bit_to_pm1(bit_value: int) -> int: return +1 if bit_value == 0 else -1

Data structure for storing results:

results[(a_setting, b_setting)] = list of (alice_out, bob_out)

results = { ('A0', 'B0'): [], ('A0', 'B1'): [], ('A1', 'B0'): [], ('A1', 'B1'): [] }

backend = AerSimulator()

for _ in range(N_RUNS): # Randomly choose Alice's measurement basis: A0 or A1 alice_choice = np.random.choice(['A0','A1']) # Randomly choose Bob's measurement basis: B0 or B1 bob_choice = np.random.choice(['B0','B1'])

# Build a circuit
#  - qr[0] = Alice's qubit
#  - qr[1] = Bob's qubit
qr = QuantumRegister(2, 'q')
cr = ClassicalRegister(2, 'c')
qc = QuantumCircuit(qr, cr)

# 1. Create Bell state (|Phi+&gt;) = (|00&gt; + |11&gt;)/sqrt(2)
qc.h(qr[0])
qc.cx(qr[0], qr[1])

# 2. Eve intercepts Bob's qubit (optional):
if EVE_ACTIVE:
    # Randomly choose Eve's measurement basis: E0 or E1
    eve_choice = np.random.choice(['E0','E1'])
    eve_angle = E0 if eve_choice == 'E0' else E1

    # Eve's rotation and measurement:
    #   - Rotate Bob's qubit into Eve's basis
    qc.ry(-eve_angle, qr[1])    # rotating by -theta =&gt; measure in &quot;theta&quot; basis
    qc.measure(qr[1], cr[1])    # measure Bob's qubit in Eve's basis
    # reset Bob's qubit and set it back to the post-measurement state
    qc.reset(qr[1])

    # c_if (conditional gates) is one approach, but here we use transpile tricks for simplicity.
    # In standard Qiskit, it is a bit more complicated to precisely restore the measured state.
    # Simpler is to ignore the measured outcome and leave Bob's qubit in, for example, |0&gt;.
    #
    # =&gt; This, however, reveals some info about the realism of Eve's action... 
    #    In a real eavesdropping, Eve would theoretically know the measured outcome
    #    and then forward exactly that state.
    #
    # For the core of the CHSH-protocol, this difference is not very important:
    #    the fact is that Eve's measurement 'disturbs' the entanglement.
    # 
    # Here we set Bob's qubit to |0&gt; after Eve's measurement and reset.

# 3. Alice measurement in A0 = 0 or A1 = pi/4:
if alice_choice == 'A0':
    angle_a = A0
else:
    angle_a = A1

qc.ry(-angle_a, qr[0])  # rotate qubit 0 from Alice's basis to Z-basis
# 4. Bob measurement in B0 = pi/8 or B1 = -pi/8:
if bob_choice == 'B0':
    angle_b = B0
else:
    angle_b = B1

qc.ry(-angle_b, qr[1])  # rotate qubit 1 from Bob's basis to Z-basis

# 5. Measure in the computational basis
qc.measure(qr[0], cr[0])  # Alice's outcome in cr[0]
qc.measure(qr[1], cr[1])  # Bob's outcome in cr[1]

# Simulate
transpiled_qc = transpile(qc, backend)
result = backend.run(transpiled_qc, shots=1).result()
counts = result.get_counts()

# There is only one outcome (shots=1), so take the only key
measured_state = list(counts.keys())[0]
# measured_state is, for example, '10' =&gt; cr[1]cr[0], so reversed indexing
# Note: the classical register index c[1] is Bob, c[0] is Alice
bob_bit = int(measured_state[0])   # MSB
alice_bit = int(measured_state[1]) # LSB

# In CHSH correlation: 0 -&gt; +1, 1 -&gt; -1
a_val = bit_to_pm1(alice_bit)
b_val = bit_to_pm1(bob_bit)

# Save in the results dict
results[(alice_choice, bob_choice)].append((a_val, b_val))

Now the results contain the outcomes per (A_i, B_j)

Function to compute E(A,B) = <a*b> from the measured data

def correlation(list_of_pairs): # list_of_pairs contains tuples (a_val, b_val) where each is in {+1, -1} if len(list_of_pairs) == 0: return 0 return sum(a*b for (a,b) in list_of_pairs) / len(list_of_pairs)

Compute E(A0,B0), E(A0,B1), E(A1,B0), E(A1,B1)

E_A0B0 = correlation(results[('A0','B0')]) E_A0B1 = correlation(results[('A0','B1')]) E_A1B0 = correlation(results[('A1','B0')]) E_A1B1 = correlation(results[('A1','B1')])

CHSH value:

Note, the 'classical' form is S = E(A0,B0) - E(A0,B1) + E(A1,B0) + E(A1,B1)

or small variants thereof, depending on the sign convention.

Note that in some sources -E(A1,B1) is used. There are different definitions,

so you can check what you want.

Here we use a common convention:

S = (E_A0B0 + E_A0B1 + E_A1B0 - E_A1B1)

Print results

print("\n=== CHSH RESULTS ===") print(f"E(A0, B0) = {E_A0B0:.3f}") print(f"E(A0, B1) = {E_A0B1:.3f}") print(f"E(A1, B0) = {E_A1B0:.3f}") print(f"E(A1, B1) = {E_A1B1:.3f}") print(f"S = {S:.3f}")

print("\nEVE_ACTIVE =", EVE_ACTIVE) print("Number of runs:", N_RUNS)

Ouput

=== CHSH RESULTS ===
E(A0, B0) = 0.919
E(A0, B1) = 0.941
E(A1, B0) = 0.949
E(A1, B1) = 0.333
S           = 2.476

EVE_ACTIVE = False Number of runs: 1000 ```

Bram
  • 645
  • 4
  • 10