Skip to content

Cryptography Protocols

Quantum cryptography protocols (crypto-q, promoted from Dense-Evolution-Discovery issue #189), built entirely from existing simulator primitives — no new quantum channel was added to the core to support this section.

BB84

The reference pattern every protocol here follows: prepare -> channel -> measure -> sift -> QBER. Validated at N=5000 rounds, 5 independent seeds: QBER=0 on a perfect channel, QBER=2p/3 under depolarizing noise at rate p, QBER=0.25 under an intercept-resend attack — all within ±1σ of theory (worst case ±0.87σ).

Three-party device-independent conference key agreement (GHZ)

Following Ribeiro, Murta & Wehner 2018 (arXiv:1708.00798): a GHZ(3) state and the paper's own "Parity-CHSH" inequality (not Mermin's — verified directly against the paper text). parity_chsh_win_rate reaches the quantum maximum P_win = 0.85355... to machine precision on an ideal channel, comfortably clearing the classical bound of 0.75. Two real bugs were found and fixed while implementing this (a fixed test question read from the wrong value; a depolarizing shrink-factor convention mismatch) — see di_qkd_ghz's own module docstring for both, since a number without that history is a number without meaning.

Multi-round DICKA structure

dicka_protocol2.run_protocol runs the full multi-round structure (Appendix Protocol 2 of the same paper) around the GHZ primitives above: round selection, parameter estimation, and the abort decision. It deliberately does not report a secure key length — Theorem 4's exact value depends on a numerical optimization (Lemma 3) the source paper never reduces to closed form, and inventing one here would not be a real number. What it reports is everything the protocol actually specifies as a physical procedure, honestly bounded at that.

bb84

BB84 quantum key distribution, built entirely from existing dense_evolution primitives (statevector, gates, measurement, the depolarizing channel) -- the reference pattern every protocol in this subpackage follows: prepare -> channel -> measure -> sift -> QBER.

Reused, not reimplemented: DenseSVSimulator, GATES, sim.measure (with jax_key), NoiseModel.apply_to_sv. No new quantum primitive is introduced.

Promoted from Dense-Evolution-Discovery's scripts/crypto/bb84.py (the crypto-q RFC, issue #189) after validation at N=5000 rounds, 5 independent seeds:

Scenario              QBER observed        Expected   z-score
Perfect channel       0.0000 +/- 0.0000    0          pass
Depolarizing p=0.05   0.0365 +/- 0.0023    0.0333     +0.87
Depolarizing p=0.10   0.0694 +/- 0.0015    0.0667     +0.56
Depolarizing p=0.20   0.1386 +/- 0.0034    0.1333     +0.77
Intercept-resend      0.2518 +/- 0.0017    0.2500     +0.20

All z-scores within +/-1 sigma except p=0.20, still comfortably within +/-2 sigma.

prepare_state

prepare_state(base: int, bit: int)

Prepares one qubit in the BB84 sender's state for a given basis and bit.

base=0 is the Z basis (|0>, |1>); base=1 is the X basis (|+>, |->), reached by applying H to the Z-basis state for the same bit.

Returns a fresh DenseSVSimulator(1) holding the prepared state.

Source code in dense_evolution/protocols/bb84.py
def prepare_state(base: int, bit: int):
    """Prepares one qubit in the BB84 sender's state for a given basis
    and bit.

    `base=0` is the Z basis (`|0>`, `|1>`); `base=1` is the X basis
    (`|+>`, `|->`), reached by applying H to the Z-basis state for the
    same bit.

    Returns a fresh `DenseSVSimulator(1)` holding the prepared state."""
    sim = de.DenseSVSimulator(1)
    if base == 0:
        if bit == 1:
            sim.apply_gate_1q(de.GATES["x"], 0)
    else:
        sim.apply_gate_1q(de.GATES["h"], 0)
        if bit == 1:
            sim.apply_gate_1q(de.GATES["z"], 0)
    return sim

measure_in_basis

measure_in_basis(sim, base: int, rng: Generator) -> int

Measures qubit 0 of sim in the Z basis (base=0) or the X basis (base=1, reached by applying H before the Z measurement).

rng seeds the single-shot measurement outcome via a freshly derived jax.random.PRNGKey. Returns the measured bit (0 or 1).

Source code in dense_evolution/protocols/bb84.py
def measure_in_basis(sim, base: int, rng: np.random.Generator) -> int:
    """Measures qubit 0 of `sim` in the Z basis (`base=0`) or the X basis
    (`base=1`, reached by applying H before the Z measurement).

    `rng` seeds the single-shot measurement outcome via a freshly derived
    `jax.random.PRNGKey`. Returns the measured bit (0 or 1)."""
    if base == 1:
        sim.apply_gate_1q(de.GATES["h"], 0)
    key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
    return int(sim.measure(0, jax_key=key))

apply_depolarizing

apply_depolarizing(sim, p: float, rng: Generator) -> None

Applies NoiseModel's real depolarizing channel to sim's statevector in place, with error probability p. A no-op at p=0.0.

Source code in dense_evolution/protocols/bb84.py
def apply_depolarizing(sim, p: float, rng: np.random.Generator) -> None:
    """Applies `NoiseModel`'s real depolarizing channel to `sim`'s
    statevector in place, with error probability `p`. A no-op at
    `p=0.0`."""
    if p == 0.0:
        return
    sv = np.asarray(sim.get_statevector())
    sv_noisy = NoiseModel.apply_to_sv(sv, 1, "depolarizing", p, rng=rng)
    sim.set_initial_state(np.asarray(sv_noisy))

bb84_round

bb84_round(
    rng: Generator,
    p_channel: float = 0.0,
    eve: bool = False,
)

Runs one BB84 round: Alice prepares a random bit in a random basis, an optional intercept-resend eavesdropper (eve=True) measures and re-prepares it in her own random basis, the channel applies depolarizing noise at rate p_channel, and Bob measures in his own random basis.

Returns (a_base, b_base, a_bit, b_bit).

Source code in dense_evolution/protocols/bb84.py
def bb84_round(rng: np.random.Generator, p_channel: float = 0.0, eve: bool = False):
    """Runs one BB84 round: Alice prepares a random bit in a random
    basis, an optional intercept-resend eavesdropper (`eve=True`) measures
    and re-prepares it in her own random basis, the channel applies
    depolarizing noise at rate `p_channel`, and Bob measures in his own
    random basis.

    Returns `(a_base, b_base, a_bit, b_bit)`."""
    a_base = int(rng.integers(0, 2))
    a_bit = int(rng.integers(0, 2))
    sim = prepare_state(a_base, a_bit)

    if eve:
        e_base = int(rng.integers(0, 2))
        e_bit = measure_in_basis(sim, e_base, rng)
        sim = prepare_state(e_base, e_bit)

    apply_depolarizing(sim, p_channel, rng)

    b_base = int(rng.integers(0, 2))
    b_bit = measure_in_basis(sim, b_base, rng)
    return a_base, b_base, a_bit, b_bit

bb84_run

bb84_run(
    n_rounds: int,
    p_channel: float = 0.0,
    eve: bool = False,
    seed=None,
)

Runs n_rounds of BB84, sifts the rounds where Alice and Bob's bases happened to match, and reports the sifted-key quantum bit error rate (QBER).

Ground truth, verified at N=5000 rounds / 5 seeds (see module docstring): QBER=0 on a perfect channel, QBER=2p/3 under isotropic depolarizing noise at rate p, and QBER=0.25 under an intercept-resend attack (eve=True) -- Eve guesses the right basis half the time, and introduces an error half the time she guesses wrong.

Returns (qber, n_sifted); qber=0.0 if no rounds sifted.

Example

import dense_evolution.protocols.bb84 as bb84 qber, n_sifted = bb84.bb84_run(2000, p_channel=0.10, seed=1) abs(qber - 2 * 0.10 / 3) < 0.03 True

Source code in dense_evolution/protocols/bb84.py
def bb84_run(n_rounds: int, p_channel: float = 0.0, eve: bool = False, seed=None):
    """Runs `n_rounds` of BB84, sifts the rounds where Alice and Bob's
    bases happened to match, and reports the sifted-key quantum bit error
    rate (QBER).

    Ground truth, verified at N=5000 rounds / 5 seeds (see module
    docstring): QBER=0 on a perfect channel, `QBER=2p/3` under isotropic
    depolarizing noise at rate `p`, and `QBER=0.25` under an
    intercept-resend attack (`eve=True`) -- Eve guesses the right basis
    half the time, and introduces an error half the time she guesses
    wrong.

    Returns `(qber, n_sifted)`; `qber=0.0` if no rounds sifted.

    Example
    -------
    >>> import dense_evolution.protocols.bb84 as bb84
    >>> qber, n_sifted = bb84.bb84_run(2000, p_channel=0.10, seed=1)
    >>> abs(qber - 2 * 0.10 / 3) < 0.03
    True
    """
    rng = np.random.default_rng(seed)
    a_bases = np.empty(n_rounds, dtype=np.int8)
    b_bases = np.empty(n_rounds, dtype=np.int8)
    a_bits = np.empty(n_rounds, dtype=np.int8)
    b_bits = np.empty(n_rounds, dtype=np.int8)

    for i in range(n_rounds):
        a_bases[i], b_bases[i], a_bits[i], b_bits[i] = bb84_round(rng, p_channel, eve)

    keep = a_bases == b_bases
    n_sifted = int(keep.sum())
    if n_sifted == 0:
        return 0.0, 0
    qber = float(np.mean(a_bits[keep] != b_bits[keep]))
    return qber, n_sifted

di_qkd_ghz

Three-party device-independent conference key agreement (DICKA) via a GHZ(3) state, following Ribeiro, Murta & Wehner 2018, "Fully device-independent conference key agreement" (arXiv:1708.00798), Protocol 1.

Ground truth, corrected against the actual paper text (not Mermin's inequality, which this paper does not use): the paper introduces its own "Parity-CHSH" inequality, an N-party extension of CHSH (Definition 8). For N=3 (Alice, Bob1, one other Bob), the winning condition is a + b1 = x*(y XOR b2) mod 2, with classical bound P_win <= 3/4 and quantum maximum P_win ~ 0.85 (the same bound as ordinary CHSH, Eq. A.14) -- both reproduced numerically, not assumed.

Two real bugs were found and fixed while promoting this from Dense-Evolution-Discovery's scripts/crypto/di_qkd_ghz.py:

  1. Definition 8's own text fixes the other Bob's test-round question at "always equal to 1", not 0 -- an initial implementation used 0 (Z measurement) and got P_win=0.677, exactly half the expected quantum boost. Fixing it to 1 (X measurement) reproduces the exact theoretical value 0.85355... to machine precision.
  2. expected_win_rate's noisy-channel formula assumed depolarizing shrinks a qubit's Pauli expectations by (1-p); NoiseModel's actual isotropic-Pauli-error Kraus channel shrinks them by (1-4p/3) instead (see expected_win_rate's own docstring).

Measurement operators, taken directly from the paper's own honest-implementation section (Eq. A.64-A.65 region):

Alice:      x=0 -> Z,                 x=1 -> X
Bob1:       y=0 -> (Z+X)/sqrt(2),      y=1 -> (Z-X)/sqrt(2),  y=2 -> Z
other Bobs: y=0 -> Z,                  y=1 -> X

Reused, not reimplemented: de.ghz_state(3), DenseSVSimulator, GATES, sim.measure (jax_key), NoiseModel.apply_to_sv. No new quantum primitive.

prepare_ghz3

prepare_ghz3()

Prepares the shared 3-qubit GHZ state (|000> + |111>)/sqrt(2) used throughout this module, via de.ghz_state(3). Returns a fresh DenseSVSimulator(3).

Source code in dense_evolution/protocols/di_qkd_ghz.py
def prepare_ghz3():
    """Prepares the shared 3-qubit GHZ state
    `(|000> + |111>)/sqrt(2)` used throughout this module, via
    `de.ghz_state(3)`. Returns a fresh `DenseSVSimulator(3)`."""
    sim = de.DenseSVSimulator(3)
    sim.run_circuit(de.ghz_state(3))
    return sim

measure_alice

measure_alice(sim, x: int, rng: Generator) -> int

Measures Alice's qubit (index 0) with question x: x=0 measures Z directly, x=1 applies H first (measures X). Returns the outcome bit.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def measure_alice(sim, x: int, rng: np.random.Generator) -> int:
    """Measures Alice's qubit (index 0) with question `x`: `x=0` measures
    Z directly, `x=1` applies H first (measures X). Returns the outcome
    bit."""
    if x == 1:
        sim.apply_gate_1q(de.GATES["h"], 0)
    key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
    return int(sim.measure(0, jax_key=key))

measure_bob1

measure_bob1(sim, y: int, rng: Generator) -> int

Measures Bob1's qubit (index 1) with question y: y=0 measures (Z+X)/sqrt(2), y=1 measures (Z-X)/sqrt(2) (both via the precomputed diagonalizing unitaries _U_PLUS/_U_MINUS), y=2 measures Z directly. Returns the outcome bit.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def measure_bob1(sim, y: int, rng: np.random.Generator) -> int:
    """Measures Bob1's qubit (index 1) with question `y`: `y=0` measures
    `(Z+X)/sqrt(2)`, `y=1` measures `(Z-X)/sqrt(2)` (both via the
    precomputed diagonalizing unitaries `_U_PLUS`/`_U_MINUS`), `y=2`
    measures Z directly. Returns the outcome bit."""
    if y == 0:
        sim.apply_gate_1q(_U_PLUS, 1)
    elif y == 1:
        sim.apply_gate_1q(_U_MINUS, 1)
    key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
    return int(sim.measure(1, jax_key=key))

measure_other_bob

measure_other_bob(
    sim, y: int, rng: Generator, qubit: int = 2
) -> int

Measures the other Bob's qubit (default index 2) with question y: y=0 measures Z directly, y=1 applies H first (measures X). Returns the outcome bit.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def measure_other_bob(sim, y: int, rng: np.random.Generator, qubit: int = 2) -> int:
    """Measures the other Bob's qubit (default index 2) with question
    `y`: `y=0` measures Z directly, `y=1` applies H first (measures X).
    Returns the outcome bit."""
    if y == 1:
        sim.apply_gate_1q(de.GATES["h"], qubit)
    key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
    return int(sim.measure(qubit, jax_key=key))

apply_depolarizing

apply_depolarizing(sim, p: float, rng: Generator) -> None

Applies i.i.d. depolarizing noise at rate p to all 3 qubits of sim, matching the paper's own D^{\otimes N}(GHZ_N) honest-implementation model (Eq. A.64). A no-op at p=0.0.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def apply_depolarizing(sim, p: float, rng: np.random.Generator) -> None:
    """Applies i.i.d. depolarizing noise at rate `p` to all 3 qubits of
    `sim`, matching the paper's own `D^{\\otimes N}(GHZ_N)`
    honest-implementation model (Eq. A.64). A no-op at `p=0.0`."""
    if p == 0.0:
        return
    sv = np.asarray(sim.get_statevector())
    sv_noisy = NoiseModel.apply_to_sv(sv, 3, "depolarizing", p, rng=rng)
    sim.set_initial_state(np.asarray(sv_noisy))

test_round

test_round(rng: Generator, p_dep: float = 0.0) -> int

Runs one Parity-CHSH test round (Definition 8): Alice and Bob1 get uniformly random questions x, y; the other Bob gets a FIXED question, always 1 (X measurement) -- per the paper's own definition text, not 0 (see module docstring for the bug this fixed).

Returns 1 if the parties win the round ((a + b1) mod 2 == (x*(y XOR b2)) mod 2), else 0.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def test_round(rng: np.random.Generator, p_dep: float = 0.0) -> int:
    """Runs one Parity-CHSH test round (Definition 8): Alice and Bob1 get
    uniformly random questions `x, y`; the other Bob gets a FIXED
    question, always 1 (X measurement) -- per the paper's own definition
    text, not 0 (see module docstring for the bug this fixed).

    Returns 1 if the parties win the round (`(a + b1) mod 2 ==
    (x*(y XOR b2)) mod 2`), else 0."""
    sim = prepare_ghz3()
    apply_depolarizing(sim, p_dep, rng)
    x = int(rng.integers(0, 2))
    y = int(rng.integers(0, 2))
    a = measure_alice(sim, x, rng)
    b1 = measure_bob1(sim, y, rng)
    b2 = measure_other_bob(sim, 1, rng)
    win = (a + b1) % 2 == (x * (y ^ b2)) % 2
    return int(win)

key_round

key_round(rng: Generator, p_dep: float = 0.0)

Runs one key-generation round: everyone measures Z (x=0, y=2, y2=0). Returns (a, b1, b2), perfectly correlated in the noiseless case.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def key_round(rng: np.random.Generator, p_dep: float = 0.0):
    """Runs one key-generation round: everyone measures Z (`x=0, y=2,
    y2=0`). Returns `(a, b1, b2)`, perfectly correlated in the noiseless
    case."""
    sim = prepare_ghz3()
    apply_depolarizing(sim, p_dep, rng)
    a = measure_alice(sim, 0, rng)
    b1 = measure_bob1(sim, 2, rng)
    b2 = measure_other_bob(sim, 0, rng)
    return a, b1, b2

parity_chsh_win_rate

parity_chsh_win_rate(
    n_rounds: int, p_dep: float = 0.0, seed=None
) -> float

Runs n_rounds Parity-CHSH test rounds and returns the observed winning frequency.

Ground truth, verified to machine precision at p_dep=0.0: P_win = 0.85355... (1/2 + 1/(2 sqrt2), the quantum maximum), well above the classical bound 3/4 -- device independence requires exceeding this classical bound.

Example

import dense_evolution.protocols.di_qkd_ghz as ghz p_win = ghz.parity_chsh_win_rate(3000, p_dep=0.0, seed=0) p_win > 0.75 True

Source code in dense_evolution/protocols/di_qkd_ghz.py
def parity_chsh_win_rate(n_rounds: int, p_dep: float = 0.0, seed=None) -> float:
    """Runs `n_rounds` Parity-CHSH test rounds and returns the observed
    winning frequency.

    Ground truth, verified to machine precision at `p_dep=0.0`:
    `P_win = 0.85355...` (`1/2 + 1/(2 sqrt2)`, the quantum maximum), well
    above the classical bound `3/4` -- device independence requires
    exceeding this classical bound.

    Example
    -------
    >>> import dense_evolution.protocols.di_qkd_ghz as ghz
    >>> p_win = ghz.parity_chsh_win_rate(3000, p_dep=0.0, seed=0)
    >>> p_win > 0.75
    True
    """
    rng = np.random.default_rng(seed)
    wins = sum(test_round(rng, p_dep) for _ in range(n_rounds))
    return wins / n_rounds

key_qber

key_qber(n_rounds: int, p_dep: float = 0.0, seed=None)

Runs n_rounds key-generation rounds and returns the raw QBER between Alice and each Bob, (qber_b1, qber_b2), before error correction or privacy amplification. Zero at p_dep=0.0, rising with p_dep.

Source code in dense_evolution/protocols/di_qkd_ghz.py
def key_qber(n_rounds: int, p_dep: float = 0.0, seed=None):
    """Runs `n_rounds` key-generation rounds and returns the raw QBER
    between Alice and each Bob, `(qber_b1, qber_b2)`, before error
    correction or privacy amplification. Zero at `p_dep=0.0`, rising with
    `p_dep`."""
    rng = np.random.default_rng(seed)
    disagree_b1 = 0
    disagree_b2 = 0
    for _ in range(n_rounds):
        a, b1, b2 = key_round(rng, p_dep)
        disagree_b1 += a != b1
        disagree_b2 += a != b2
    return disagree_b1 / n_rounds, disagree_b2 / n_rounds

expected_win_rate

expected_win_rate(
    p_dep: float, n_parties: int = 3
) -> float

Closed-form honest-implementation Parity-CHSH win rate (paper Eq. A.65, specialized to n_parties=3), written in terms of the per-qubit Pauli-expectation shrink factor s a depolarizing channel produces: p_exp = 1/2 + s^3/(2 sqrt2) + s^2*(1-s)/(4 sqrt2).

s = 1 - 4*p_dep/3, matching NoiseModel's real isotropic-Pauli depolarizing Kraus map (K0=sqrt(1-p)I, K1..3=sqrt(p/3)*Pauli), NOT s=1-p (which would assume a "replace with the maximally mixed state" channel instead). Verified directly against the simulator: the s=1-p version diverged with growing significance as p grew (z=-2.75 at p=0.10, z=-3.65 at p=0.15, N=3000); s=1-4p/3 matches the simulator across the whole sweep (|z|<0.5 everywhere tested).

Source code in dense_evolution/protocols/di_qkd_ghz.py
def expected_win_rate(p_dep: float, n_parties: int = 3) -> float:
    """Closed-form honest-implementation Parity-CHSH win rate (paper Eq.
    A.65, specialized to `n_parties=3`), written in terms of the
    per-qubit Pauli-expectation shrink factor `s` a depolarizing channel
    produces: `p_exp = 1/2 + s^3/(2 sqrt2) + s^2*(1-s)/(4 sqrt2)`.

    `s = 1 - 4*p_dep/3`, matching `NoiseModel`'s real isotropic-Pauli
    depolarizing Kraus map (`K0=sqrt(1-p)I, K1..3=sqrt(p/3)*Pauli`), NOT
    `s=1-p` (which would assume a "replace with the maximally mixed
    state" channel instead). Verified directly against the simulator: the
    `s=1-p` version diverged with growing significance as `p` grew
    (`z=-2.75` at `p=0.10`, `z=-3.65` at `p=0.15`, N=3000); `s=1-4p/3`
    matches the simulator across the whole sweep (`|z|<0.5` everywhere
    tested)."""
    s = 1 - 4 * p_dep / 3
    s2 = np.sqrt(2)
    return 0.5 + s ** n_parties / (2 * s2) + s ** 2 * (1 - s ** (n_parties - 2)) / (4 * s2)

dicka_protocol2

The full multi-round DICKA structure from Ribeiro, Murta & Wehner 2018 (arXiv:1708.00798), Appendix Protocol 2 -- "a more detailed version of Protocol 1". Builds directly on di_qkd_ghz's already-validated single-round primitives (GHZ(3) preparation, measurements, depolarizing noise, the Parity-CHSH game, the all-Z key round); this module adds the round-selection and parameter-estimation layer around them.

Per-round structure (paper's own text, step 1):

(a) prepare the shared GHZ(3) state.
(b) Alice picks `T_i ~ Bernoulli(gamma)` and announces it.
(c) `T_i=0` -> a key round: everyone measures Z (`x, y, y2 = 0, 2,
    0`). `T_i=1` -> a test round: Alice/Bob1 get random questions,
    the other Bob gets question=1 (Definition 8) -- the Parity-CHSH
    game.

Repeated for n rounds; the protocol aborts if the observed winning frequency on test rounds falls below a threshold beta in ]3/4, 1/2+1/(2 sqrt2)[.

What this does NOT reproduce, and why: Theorem 4's exact secure key length l is a function of f-tilde(beta), a bound on the single-round von Neumann entropy defined in the paper's own Lemma 3 as the unique tangent to a convex function at an optimized point p_opt -- not a closed-form expression, but the output of a separate numerical (SDP-style) optimization the paper carries out on its own. Inventing a number for it here would mean reporting something the paper never actually gives in closed form. What IS reproduced, against the real simulator, is everything Protocol 2 actually specifies as a physical procedure: the round selection, the honest-implementation winning frequency (parameter estimation), the abort decision, and the raw key's QBER before error correction -- i.e. everything up to, but not including, the privacy-amplification key-rate number.

run_round

run_round(rng: Generator, gamma: float, p_dep: float = 0.0)

Runs one round of Protocol 2, step 1(a)-(c): prepares the shared GHZ(3) state, applies depolarizing noise at rate p_dep, then with probability gamma runs a Parity-CHSH test round, otherwise a key-generation round.

Returns ("test", win) or ("key", a, b1, b2).

Source code in dense_evolution/protocols/dicka_protocol2.py
def run_round(rng: np.random.Generator, gamma: float, p_dep: float = 0.0):
    """Runs one round of Protocol 2, step 1(a)-(c): prepares the shared
    GHZ(3) state, applies depolarizing noise at rate `p_dep`, then with
    probability `gamma` runs a Parity-CHSH test round, otherwise a
    key-generation round.

    Returns `("test", win)` or `("key", a, b1, b2)`."""
    sim = prepare_ghz3()
    apply_depolarizing(sim, p_dep, rng)
    if rng.random() < gamma:
        x = int(rng.integers(0, 2))
        y = int(rng.integers(0, 2))
        a = measure_alice(sim, x, rng)
        b1 = measure_bob1(sim, y, rng)
        b2 = measure_other_bob(sim, 1, rng)
        win = int((a + b1) % 2 == (x * (y ^ b2)) % 2)
        return ("test", win)
    a = measure_alice(sim, 0, rng)
    b1 = measure_bob1(sim, 2, rng)
    b2 = measure_other_bob(sim, 0, rng)
    return ("key", a, b1, b2)

run_protocol

run_protocol(
    n_rounds: int,
    gamma: float,
    beta: float,
    p_dep: float = 0.0,
    seed=None,
) -> dict

Runs the full multi-round DICKA structure for n_rounds, then applies the abort rule from step 1: abort if the observed test-round win frequency p_hat is below beta.

beta must sit strictly between CLASSICAL_BOUND (0.75) and QUANTUM_MAX (0.5 + 1/(2 sqrt2) ~= 0.8536) to meaningfully distinguish a device-independent channel from a classically-bound one.

Returns a dict with everything the protocol itself produces -- n_rounds, n_test, n_key, p_hat, beta, aborted, qber_b1, qber_b2 -- not the privacy-amplified final key length (see module docstring for why).

Example

import dense_evolution.protocols.dicka_protocol2 as dicka beta = (dicka.CLASSICAL_BOUND + dicka.QUANTUM_MAX) / 2 result = dicka.run_protocol(1000, gamma=0.7, beta=beta, p_dep=0.0, seed=0) bool(result["aborted"]) False

Source code in dense_evolution/protocols/dicka_protocol2.py
def run_protocol(n_rounds: int, gamma: float, beta: float, p_dep: float = 0.0, seed=None) -> dict:
    """Runs the full multi-round DICKA structure for `n_rounds`, then
    applies the abort rule from step 1: abort if the observed test-round
    win frequency `p_hat` is below `beta`.

    `beta` must sit strictly between `CLASSICAL_BOUND` (0.75) and
    `QUANTUM_MAX` (`0.5 + 1/(2 sqrt2) ~= 0.8536`) to meaningfully
    distinguish a device-independent channel from a classically-bound
    one.

    Returns a dict with everything the protocol itself produces --
    `n_rounds`, `n_test`, `n_key`, `p_hat`, `beta`, `aborted`, `qber_b1`,
    `qber_b2` -- not the privacy-amplified final key length (see module
    docstring for why).

    Example
    -------
    >>> import dense_evolution.protocols.dicka_protocol2 as dicka
    >>> beta = (dicka.CLASSICAL_BOUND + dicka.QUANTUM_MAX) / 2
    >>> result = dicka.run_protocol(1000, gamma=0.7, beta=beta, p_dep=0.0, seed=0)
    >>> bool(result["aborted"])
    False
    """
    rng = np.random.default_rng(seed)
    test_wins = test_total = 0
    key_a, key_b1, key_b2 = [], [], []
    for _ in range(n_rounds):
        outcome = run_round(rng, gamma, p_dep)
        if outcome[0] == "test":
            test_total += 1
            test_wins += outcome[1]
        else:
            _, a, b1, b2 = outcome
            key_a.append(a)
            key_b1.append(b1)
            key_b2.append(b2)

    p_hat = test_wins / test_total if test_total else 0.0
    aborted = p_hat < beta
    key_a, key_b1, key_b2 = map(np.array, (key_a, key_b1, key_b2))
    qber_b1 = float(np.mean(key_a != key_b1)) if len(key_a) else 0.0
    qber_b2 = float(np.mean(key_a != key_b2)) if len(key_a) else 0.0

    return {
        "n_rounds": n_rounds,
        "n_test": test_total,
        "n_key": len(key_a),
        "p_hat": p_hat,
        "beta": beta,
        "aborted": aborted,
        "qber_b1": qber_b1,
        "qber_b2": qber_b2,
    }