dense-evolution 8.3.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- mcp_server/utils/truncation.py +38 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Gauge-safe gradients for spectral functions of a Hermitian matrix
|
|
2
|
+
(V f(Lambda) V^dagger, e.g. time evolution exp(-iHt)) at exact eigenvalue
|
|
3
|
+
degeneracy.
|
|
4
|
+
|
|
5
|
+
`jnp.linalg.eigh`'s reverse-mode gradient divides by `lambda_i - lambda_j`
|
|
6
|
+
for every eigenvector pair. When two eigenvalues are exactly degenerate,
|
|
7
|
+
this does not raise and does not always produce NaN -- it can silently
|
|
8
|
+
return a finite, WRONG gradient, because the eigenvectors spanning a
|
|
9
|
+
degenerate eigenspace are not themselves uniquely defined (any orthonormal
|
|
10
|
+
basis of that subspace is an equally valid `eigh` output). Measured on a
|
|
11
|
+
real Kaggle CPU kernel (Dense-Evolution-Discovery, PR #173): std eigh
|
|
12
|
+
gradient error 0.98 vs Kato 4e-10 on an H with four exact doubly-degenerate
|
|
13
|
+
eigenvalues -- several orders of magnitude, not a rounding difference.
|
|
14
|
+
|
|
15
|
+
REFERENCES (verified against the actual paper text, not trusted at face
|
|
16
|
+
value from a citation string alone):
|
|
17
|
+
Kasim, M. F., "Derivatives of partial eigendecomposition of a real
|
|
18
|
+
symmetric matrix for degenerate cases", arXiv:2011.04366 (2020).
|
|
19
|
+
Kato, T., "Perturbation Theory for Linear Operators", Springer (1995),
|
|
20
|
+
Ch. II.5.6 (the classical divided-difference formula for matrix
|
|
21
|
+
function derivatives, predating Kasim by decades).
|
|
22
|
+
|
|
23
|
+
`matrix_function_eigh` uses a `jax.custom_jvp` based on Kato's
|
|
24
|
+
divided-difference formula for matrix functions:
|
|
25
|
+
|
|
26
|
+
d/deps [ V(eps) f(Lambda(eps)) V(eps)^dagger ] = V (F o (V^dagger dH V)) V^dagger
|
|
27
|
+
|
|
28
|
+
with F the matrix of divided differences of f:
|
|
29
|
+
|
|
30
|
+
F[i,j] = (f(lambda_i) - f(lambda_j)) / (lambda_i - lambda_j) if lambda_i != lambda_j
|
|
31
|
+
F[i,j] = f'(lambda_i) if lambda_i == lambda_j (incl. i == j)
|
|
32
|
+
|
|
33
|
+
This formula does not pass through eigenvectors as an intermediate OUTPUT,
|
|
34
|
+
so it is gauge-invariant: the contribution from a degenerate block uses
|
|
35
|
+
f'(lambda) directly, and there is no gauge choice to make -- unlike a
|
|
36
|
+
`custom_vjp` built directly on top of `eigh`'s own eigenvector output,
|
|
37
|
+
which needs a compatibility condition on the perturbation direction
|
|
38
|
+
(Kasim's Eq. 4.72, confirmed present in the actual paper text) that this
|
|
39
|
+
formula does not.
|
|
40
|
+
|
|
41
|
+
WHEN TO USE THIS:
|
|
42
|
+
- H has (or might have) exactly degenerate eigenvalues, AND
|
|
43
|
+
- the function L(H) you differentiate depends on H through eigh, AND
|
|
44
|
+
- the function is not trivially constant on degenerate blocks.
|
|
45
|
+
|
|
46
|
+
For a Hamiltonian with only near-degeneracy (e.g. min_gap ~1e-5, no exact
|
|
47
|
+
tie), plain `jnp.linalg.eigh` is correct and faster -- use
|
|
48
|
+
`has_exact_degeneracy` to check before reaching for `spectral_evolve`.
|
|
49
|
+
"""
|
|
50
|
+
import functools
|
|
51
|
+
|
|
52
|
+
import jax
|
|
53
|
+
import jax.numpy as jnp
|
|
54
|
+
|
|
55
|
+
from dense_evolution.config import ensure_x64
|
|
56
|
+
|
|
57
|
+
__all__ = ["has_exact_degeneracy", "matrix_function_eigh", "spectral_evolve"]
|
|
58
|
+
|
|
59
|
+
_DEGENERACY_TOL = 1e-8
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def has_exact_degeneracy(H: jax.Array, tol: float = _DEGENERACY_TOL) -> bool:
|
|
63
|
+
"""True if H has at least one pair of eigenvalues closer than tol.
|
|
64
|
+
|
|
65
|
+
Diagnostic only -- call this before choosing spectral_evolve (Kato)
|
|
66
|
+
over plain jnp.linalg.eigh (std). The threshold is the same one the
|
|
67
|
+
JVP rule below uses internally, so this is the exact condition under
|
|
68
|
+
which the two methods disagree."""
|
|
69
|
+
ensure_x64()
|
|
70
|
+
w = jnp.linalg.eigvalsh(H)
|
|
71
|
+
gaps = jnp.abs(jnp.diff(jnp.sort(w)))
|
|
72
|
+
return bool((gaps < tol).any())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _matrix_function_divided_differences(fw, f_prime_w, w, tol):
|
|
76
|
+
lam_i = w[:, None]
|
|
77
|
+
lam_j = w[None, :]
|
|
78
|
+
gap = lam_i - lam_j
|
|
79
|
+
is_deg = jnp.abs(gap) < tol
|
|
80
|
+
safe_gap = jnp.where(is_deg, 1.0, gap)
|
|
81
|
+
F_quot = (fw[:, None] - fw[None, :]) / safe_gap
|
|
82
|
+
F_limit = f_prime_w[:, None]
|
|
83
|
+
return jnp.where(is_deg, F_limit, F_quot)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@functools.partial(jax.custom_jvp, nondiff_argnums=(1, 2))
|
|
87
|
+
def matrix_function_eigh(H: jax.Array, f, f_prime) -> jax.Array:
|
|
88
|
+
"""V f(Lambda) V^dagger with a gauge-safe gradient at exact degeneracy.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
H : (n, n) Hermitian matrix.
|
|
93
|
+
f : callable, lambda (array) -> array. Applied elementwise to the
|
|
94
|
+
eigenvalues.
|
|
95
|
+
f_prime : callable, lambda (array) -> array. Analytic derivative of f,
|
|
96
|
+
used only in the JVP rule for degenerate blocks. Passed via
|
|
97
|
+
nondiff_argnums since a Python closure is not a valid JAX type to
|
|
98
|
+
trace.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
(n, n) complex128 matrix.
|
|
103
|
+
|
|
104
|
+
Example
|
|
105
|
+
-------
|
|
106
|
+
>>> import jax.numpy as jnp
|
|
107
|
+
>>> H = jnp.diag(jnp.array([1.0, 1.0, 2.0, 2.0], dtype=jnp.complex128))
|
|
108
|
+
>>> U = matrix_function_eigh(H, lambda w: jnp.exp(-1j * w), lambda w: -1j * jnp.exp(-1j * w))
|
|
109
|
+
"""
|
|
110
|
+
ensure_x64()
|
|
111
|
+
w, v = jnp.linalg.eigh(H)
|
|
112
|
+
return v @ jnp.diag(f(w)) @ v.conj().T
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@matrix_function_eigh.defjvp
|
|
116
|
+
def _matrix_function_eigh_jvp(f, f_prime, primals, tangents):
|
|
117
|
+
(H,) = primals
|
|
118
|
+
(dH,) = tangents
|
|
119
|
+
|
|
120
|
+
w, v = jnp.linalg.eigh(H)
|
|
121
|
+
fw = f(w)
|
|
122
|
+
f_prime_w = f_prime(w)
|
|
123
|
+
|
|
124
|
+
F = _matrix_function_divided_differences(fw, f_prime_w, w, _DEGENERACY_TOL)
|
|
125
|
+
|
|
126
|
+
X = v.conj().T @ dH @ v
|
|
127
|
+
dU = v @ (F * X) @ v.conj().T
|
|
128
|
+
U = v @ jnp.diag(fw) @ v.conj().T
|
|
129
|
+
return U, dU
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def spectral_evolve(H: jax.Array, t: float) -> jax.Array:
|
|
133
|
+
"""exp(-i H t) with a gauge-safe gradient at exact degeneracy.
|
|
134
|
+
|
|
135
|
+
Equivalent forward to `V @ diag(exp(-1j*w*t)) @ V.conj().T` where
|
|
136
|
+
(w, V) is `jnp.linalg.eigh(H)`. Backward uses Kato's divided-difference
|
|
137
|
+
rule (see module docstring) instead of `eigh`'s own reverse-mode rule.
|
|
138
|
+
"""
|
|
139
|
+
return matrix_function_eigh(
|
|
140
|
+
H,
|
|
141
|
+
f=lambda w: jnp.exp(-1j * w * t),
|
|
142
|
+
f_prime=lambda w: -1j * t * jnp.exp(-1j * w * t),
|
|
143
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common state-preparation circuits, returned as gate-tuple lists ready to
|
|
3
|
+
feed straight into `run_circuit` (or to concatenate with more gates first).
|
|
4
|
+
The GHZ-state snippet in particular -- `[('h', 0), ('cx', 0, 1), ('cx', 1,
|
|
5
|
+
2), ...]` -- shows up hand-written at the top of practically every
|
|
6
|
+
experiment and test script built on this package; `ghz_state` is that
|
|
7
|
+
snippet, written once.
|
|
8
|
+
"""
|
|
9
|
+
from ..circuits.topology import entangling_layer
|
|
10
|
+
|
|
11
|
+
__all__ = ['ghz_state']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ghz_state(n_qubits):
|
|
15
|
+
"""
|
|
16
|
+
Build the GHZ-state preparation circuit:
|
|
17
|
+
(|00...0> + |11...1>) / sqrt(2).
|
|
18
|
+
|
|
19
|
+
Implementation: H on qubit 0, then a linear CX chain (qubit 0 -> 1,
|
|
20
|
+
1 -> 2, ..., n-2 -> n-1) propagating the superposition outward --
|
|
21
|
+
reuses `entangling_layer(n_qubits, pattern='linear')` for the chain.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
n_qubits : int
|
|
26
|
+
Number of qubits, must be >= 2 (a single qubit has no partner to
|
|
27
|
+
entangle with, so an n=1 "GHZ state" is undefined here).
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
list[tuple]
|
|
32
|
+
e.g. ghz_state(3) == [('h', 0), ('cx', 0, 1), ('cx', 1, 2)]
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> import dense_evolution as de
|
|
37
|
+
>>> sim = de.DenseSVSimulator(3)
|
|
38
|
+
>>> sim.run_circuit(de.ghz_state(3))
|
|
39
|
+
>>> sim.get_probabilities()[[0, 7]] # |000> and |111>, each 0.5
|
|
40
|
+
"""
|
|
41
|
+
if n_qubits < 2:
|
|
42
|
+
raise ValueError(f"ghz_state needs at least 2 qubits, got {n_qubits}")
|
|
43
|
+
return [('h', 0)] + entangling_layer(n_qubits, pattern='linear', gate='cx')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Quantum cryptography protocols subpackage (crypto-q, issue #189 in
|
|
2
|
+
Dense-Evolution-Discovery): BB84 (`bb84`), three-party device-independent
|
|
3
|
+
conference key agreement via a GHZ(3) state and its Parity-CHSH game
|
|
4
|
+
(`di_qkd_ghz`), and the full multi-round DICKA structure built on top of
|
|
5
|
+
it (`dicka_protocol2`). Each is built entirely from existing simulator
|
|
6
|
+
primitives (statevector, gates, measurement, the depolarizing channel) --
|
|
7
|
+
no new quantum channel was added to the core to support this subpackage.
|
|
8
|
+
|
|
9
|
+
Promoted from Dense-Evolution-Discovery only after passing that project's
|
|
10
|
+
own promotion checklist: a theoretical ground truth verified to tight
|
|
11
|
+
tolerance, a falsifiable result, primitives-only implementation, fixed-
|
|
12
|
+
seed reproducible tests, and any negative result documented honestly
|
|
13
|
+
rather than adjusted away. See each module's own docstring for its
|
|
14
|
+
specific validation numbers."""
|
|
15
|
+
from .bb84 import bb84_run, bb84_round, prepare_state, measure_in_basis, apply_depolarizing
|
|
16
|
+
from .di_qkd_ghz import (
|
|
17
|
+
prepare_ghz3, measure_alice, measure_bob1, measure_other_bob,
|
|
18
|
+
test_round, key_round, parity_chsh_win_rate, key_qber, expected_win_rate,
|
|
19
|
+
)
|
|
20
|
+
from .dicka_protocol2 import run_round, run_protocol, CLASSICAL_BOUND, QUANTUM_MAX
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"bb84_run", "bb84_round", "prepare_state", "measure_in_basis", "apply_depolarizing",
|
|
24
|
+
"prepare_ghz3", "measure_alice", "measure_bob1", "measure_other_bob",
|
|
25
|
+
"test_round", "key_round", "parity_chsh_win_rate", "key_qber", "expected_win_rate",
|
|
26
|
+
"run_round", "run_protocol", "CLASSICAL_BOUND", "QUANTUM_MAX",
|
|
27
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""BB84 quantum key distribution, built entirely from existing
|
|
2
|
+
dense_evolution primitives (statevector, gates, measurement, the
|
|
3
|
+
depolarizing channel) -- the reference pattern every protocol in this
|
|
4
|
+
subpackage follows: prepare -> channel -> measure -> sift -> QBER.
|
|
5
|
+
|
|
6
|
+
Reused, not reimplemented: `DenseSVSimulator`, `GATES`, `sim.measure`
|
|
7
|
+
(with `jax_key`), `NoiseModel.apply_to_sv`. No new quantum primitive is
|
|
8
|
+
introduced.
|
|
9
|
+
|
|
10
|
+
Promoted from Dense-Evolution-Discovery's `scripts/crypto/bb84.py` (the
|
|
11
|
+
crypto-q RFC, issue #189) after validation at N=5000 rounds, 5
|
|
12
|
+
independent seeds:
|
|
13
|
+
|
|
14
|
+
Scenario QBER observed Expected z-score
|
|
15
|
+
Perfect channel 0.0000 +/- 0.0000 0 pass
|
|
16
|
+
Depolarizing p=0.05 0.0365 +/- 0.0023 0.0333 +0.87
|
|
17
|
+
Depolarizing p=0.10 0.0694 +/- 0.0015 0.0667 +0.56
|
|
18
|
+
Depolarizing p=0.20 0.1386 +/- 0.0034 0.1333 +0.77
|
|
19
|
+
Intercept-resend 0.2518 +/- 0.0017 0.2500 +0.20
|
|
20
|
+
|
|
21
|
+
All z-scores within +/-1 sigma except p=0.20, still comfortably within
|
|
22
|
+
+/-2 sigma."""
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import jax
|
|
26
|
+
|
|
27
|
+
import dense_evolution as de
|
|
28
|
+
from dense_evolution.noise import NoiseModel
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_state(base: int, bit: int):
|
|
32
|
+
"""Prepares one qubit in the BB84 sender's state for a given basis
|
|
33
|
+
and bit.
|
|
34
|
+
|
|
35
|
+
`base=0` is the Z basis (`|0>`, `|1>`); `base=1` is the X basis
|
|
36
|
+
(`|+>`, `|->`), reached by applying H to the Z-basis state for the
|
|
37
|
+
same bit.
|
|
38
|
+
|
|
39
|
+
Returns a fresh `DenseSVSimulator(1)` holding the prepared state."""
|
|
40
|
+
sim = de.DenseSVSimulator(1)
|
|
41
|
+
if base == 0:
|
|
42
|
+
if bit == 1:
|
|
43
|
+
sim.apply_gate_1q(de.GATES["x"], 0)
|
|
44
|
+
else:
|
|
45
|
+
sim.apply_gate_1q(de.GATES["h"], 0)
|
|
46
|
+
if bit == 1:
|
|
47
|
+
sim.apply_gate_1q(de.GATES["z"], 0)
|
|
48
|
+
return sim
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def measure_in_basis(sim, base: int, rng: np.random.Generator) -> int:
|
|
52
|
+
"""Measures qubit 0 of `sim` in the Z basis (`base=0`) or the X basis
|
|
53
|
+
(`base=1`, reached by applying H before the Z measurement).
|
|
54
|
+
|
|
55
|
+
`rng` seeds the single-shot measurement outcome via a freshly derived
|
|
56
|
+
`jax.random.PRNGKey`. Returns the measured bit (0 or 1)."""
|
|
57
|
+
if base == 1:
|
|
58
|
+
sim.apply_gate_1q(de.GATES["h"], 0)
|
|
59
|
+
key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
|
|
60
|
+
return int(sim.measure(0, jax_key=key))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def apply_depolarizing(sim, p: float, rng: np.random.Generator) -> None:
|
|
64
|
+
"""Applies `NoiseModel`'s real depolarizing channel to `sim`'s
|
|
65
|
+
statevector in place, with error probability `p`. A no-op at
|
|
66
|
+
`p=0.0`."""
|
|
67
|
+
if p == 0.0:
|
|
68
|
+
return
|
|
69
|
+
sv = np.asarray(sim.get_statevector())
|
|
70
|
+
sv_noisy = NoiseModel.apply_to_sv(sv, 1, "depolarizing", p, rng=rng)
|
|
71
|
+
sim.set_initial_state(np.asarray(sv_noisy))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def bb84_round(rng: np.random.Generator, p_channel: float = 0.0, eve: bool = False):
|
|
75
|
+
"""Runs one BB84 round: Alice prepares a random bit in a random
|
|
76
|
+
basis, an optional intercept-resend eavesdropper (`eve=True`) measures
|
|
77
|
+
and re-prepares it in her own random basis, the channel applies
|
|
78
|
+
depolarizing noise at rate `p_channel`, and Bob measures in his own
|
|
79
|
+
random basis.
|
|
80
|
+
|
|
81
|
+
Returns `(a_base, b_base, a_bit, b_bit)`."""
|
|
82
|
+
a_base = int(rng.integers(0, 2))
|
|
83
|
+
a_bit = int(rng.integers(0, 2))
|
|
84
|
+
sim = prepare_state(a_base, a_bit)
|
|
85
|
+
|
|
86
|
+
if eve:
|
|
87
|
+
e_base = int(rng.integers(0, 2))
|
|
88
|
+
e_bit = measure_in_basis(sim, e_base, rng)
|
|
89
|
+
sim = prepare_state(e_base, e_bit)
|
|
90
|
+
|
|
91
|
+
apply_depolarizing(sim, p_channel, rng)
|
|
92
|
+
|
|
93
|
+
b_base = int(rng.integers(0, 2))
|
|
94
|
+
b_bit = measure_in_basis(sim, b_base, rng)
|
|
95
|
+
return a_base, b_base, a_bit, b_bit
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def bb84_run(n_rounds: int, p_channel: float = 0.0, eve: bool = False, seed=None):
|
|
99
|
+
"""Runs `n_rounds` of BB84, sifts the rounds where Alice and Bob's
|
|
100
|
+
bases happened to match, and reports the sifted-key quantum bit error
|
|
101
|
+
rate (QBER).
|
|
102
|
+
|
|
103
|
+
Ground truth, verified at N=5000 rounds / 5 seeds (see module
|
|
104
|
+
docstring): QBER=0 on a perfect channel, `QBER=2p/3` under isotropic
|
|
105
|
+
depolarizing noise at rate `p`, and `QBER=0.25` under an
|
|
106
|
+
intercept-resend attack (`eve=True`) -- Eve guesses the right basis
|
|
107
|
+
half the time, and introduces an error half the time she guesses
|
|
108
|
+
wrong.
|
|
109
|
+
|
|
110
|
+
Returns `(qber, n_sifted)`; `qber=0.0` if no rounds sifted.
|
|
111
|
+
|
|
112
|
+
Example
|
|
113
|
+
-------
|
|
114
|
+
>>> import dense_evolution.protocols.bb84 as bb84
|
|
115
|
+
>>> qber, n_sifted = bb84.bb84_run(2000, p_channel=0.10, seed=1)
|
|
116
|
+
>>> abs(qber - 2 * 0.10 / 3) < 0.03
|
|
117
|
+
True
|
|
118
|
+
"""
|
|
119
|
+
rng = np.random.default_rng(seed)
|
|
120
|
+
a_bases = np.empty(n_rounds, dtype=np.int8)
|
|
121
|
+
b_bases = np.empty(n_rounds, dtype=np.int8)
|
|
122
|
+
a_bits = np.empty(n_rounds, dtype=np.int8)
|
|
123
|
+
b_bits = np.empty(n_rounds, dtype=np.int8)
|
|
124
|
+
|
|
125
|
+
for i in range(n_rounds):
|
|
126
|
+
a_bases[i], b_bases[i], a_bits[i], b_bits[i] = bb84_round(rng, p_channel, eve)
|
|
127
|
+
|
|
128
|
+
keep = a_bases == b_bases
|
|
129
|
+
n_sifted = int(keep.sum())
|
|
130
|
+
if n_sifted == 0:
|
|
131
|
+
return 0.0, 0
|
|
132
|
+
qber = float(np.mean(a_bits[keep] != b_bits[keep]))
|
|
133
|
+
return qber, n_sifted
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Three-party device-independent conference key agreement (DICKA) via a
|
|
2
|
+
GHZ(3) state, following Ribeiro, Murta & Wehner 2018, "Fully
|
|
3
|
+
device-independent conference key agreement" (arXiv:1708.00798),
|
|
4
|
+
Protocol 1.
|
|
5
|
+
|
|
6
|
+
Ground truth, corrected against the actual paper text (not Mermin's
|
|
7
|
+
inequality, which this paper does not use): the paper introduces its own
|
|
8
|
+
"Parity-CHSH" inequality, an N-party extension of CHSH (Definition 8).
|
|
9
|
+
For N=3 (Alice, Bob1, one other Bob), the winning condition is
|
|
10
|
+
`a + b1 = x*(y XOR b2) mod 2`, with classical bound `P_win <= 3/4` and
|
|
11
|
+
quantum maximum `P_win ~ 0.85` (the same bound as ordinary CHSH, Eq.
|
|
12
|
+
A.14) -- both reproduced numerically, not assumed.
|
|
13
|
+
|
|
14
|
+
Two real bugs were found and fixed while promoting this from
|
|
15
|
+
Dense-Evolution-Discovery's `scripts/crypto/di_qkd_ghz.py`:
|
|
16
|
+
|
|
17
|
+
1. Definition 8's own text fixes the other Bob's test-round question at
|
|
18
|
+
"always equal to 1", not 0 -- an initial implementation used 0 (Z
|
|
19
|
+
measurement) and got `P_win=0.677`, exactly half the expected quantum
|
|
20
|
+
boost. Fixing it to 1 (X measurement) reproduces the exact theoretical
|
|
21
|
+
value `0.85355...` to machine precision.
|
|
22
|
+
2. `expected_win_rate`'s noisy-channel formula assumed depolarizing
|
|
23
|
+
shrinks a qubit's Pauli expectations by `(1-p)`; `NoiseModel`'s actual
|
|
24
|
+
isotropic-Pauli-error Kraus channel shrinks them by `(1-4p/3)` instead
|
|
25
|
+
(see `expected_win_rate`'s own docstring).
|
|
26
|
+
|
|
27
|
+
Measurement operators, taken directly from the paper's own
|
|
28
|
+
honest-implementation section (Eq. A.64-A.65 region):
|
|
29
|
+
|
|
30
|
+
Alice: x=0 -> Z, x=1 -> X
|
|
31
|
+
Bob1: y=0 -> (Z+X)/sqrt(2), y=1 -> (Z-X)/sqrt(2), y=2 -> Z
|
|
32
|
+
other Bobs: y=0 -> Z, y=1 -> X
|
|
33
|
+
|
|
34
|
+
Reused, not reimplemented: `de.ghz_state(3)`, `DenseSVSimulator`,
|
|
35
|
+
`GATES`, `sim.measure` (jax_key), `NoiseModel.apply_to_sv`. No new
|
|
36
|
+
quantum primitive."""
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import jax
|
|
40
|
+
|
|
41
|
+
import dense_evolution as de
|
|
42
|
+
from dense_evolution.noise import NoiseModel
|
|
43
|
+
|
|
44
|
+
_Z = np.diag([1, -1]).astype(complex)
|
|
45
|
+
_X = np.array([[0, 1], [1, 0]], dtype=complex)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _diagonalizing_unitary(observable):
|
|
49
|
+
"""The unitary `U` such that measuring Z after applying `U` to the
|
|
50
|
+
state reproduces measuring `observable` directly -- derived from
|
|
51
|
+
`observable`'s own eigendecomposition (eigenvalue +1 first, to match
|
|
52
|
+
`Z=diag(1,-1)`), not a hand-picked rotation angle."""
|
|
53
|
+
w, v = np.linalg.eigh(observable)
|
|
54
|
+
order = np.argsort(-w)
|
|
55
|
+
v = v[:, order]
|
|
56
|
+
return v.conj().T
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_U_PLUS = _diagonalizing_unitary((_Z + _X) / np.sqrt(2))
|
|
60
|
+
_U_MINUS = _diagonalizing_unitary((_Z - _X) / np.sqrt(2))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def prepare_ghz3():
|
|
64
|
+
"""Prepares the shared 3-qubit GHZ state
|
|
65
|
+
`(|000> + |111>)/sqrt(2)` used throughout this module, via
|
|
66
|
+
`de.ghz_state(3)`. Returns a fresh `DenseSVSimulator(3)`."""
|
|
67
|
+
sim = de.DenseSVSimulator(3)
|
|
68
|
+
sim.run_circuit(de.ghz_state(3))
|
|
69
|
+
return sim
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def measure_alice(sim, x: int, rng: np.random.Generator) -> int:
|
|
73
|
+
"""Measures Alice's qubit (index 0) with question `x`: `x=0` measures
|
|
74
|
+
Z directly, `x=1` applies H first (measures X). Returns the outcome
|
|
75
|
+
bit."""
|
|
76
|
+
if x == 1:
|
|
77
|
+
sim.apply_gate_1q(de.GATES["h"], 0)
|
|
78
|
+
key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
|
|
79
|
+
return int(sim.measure(0, jax_key=key))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def measure_bob1(sim, y: int, rng: np.random.Generator) -> int:
|
|
83
|
+
"""Measures Bob1's qubit (index 1) with question `y`: `y=0` measures
|
|
84
|
+
`(Z+X)/sqrt(2)`, `y=1` measures `(Z-X)/sqrt(2)` (both via the
|
|
85
|
+
precomputed diagonalizing unitaries `_U_PLUS`/`_U_MINUS`), `y=2`
|
|
86
|
+
measures Z directly. Returns the outcome bit."""
|
|
87
|
+
if y == 0:
|
|
88
|
+
sim.apply_gate_1q(_U_PLUS, 1)
|
|
89
|
+
elif y == 1:
|
|
90
|
+
sim.apply_gate_1q(_U_MINUS, 1)
|
|
91
|
+
key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
|
|
92
|
+
return int(sim.measure(1, jax_key=key))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def measure_other_bob(sim, y: int, rng: np.random.Generator, qubit: int = 2) -> int:
|
|
96
|
+
"""Measures the other Bob's qubit (default index 2) with question
|
|
97
|
+
`y`: `y=0` measures Z directly, `y=1` applies H first (measures X).
|
|
98
|
+
Returns the outcome bit."""
|
|
99
|
+
if y == 1:
|
|
100
|
+
sim.apply_gate_1q(de.GATES["h"], qubit)
|
|
101
|
+
key = jax.random.PRNGKey(int(rng.integers(0, 2**31 - 1)))
|
|
102
|
+
return int(sim.measure(qubit, jax_key=key))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def apply_depolarizing(sim, p: float, rng: np.random.Generator) -> None:
|
|
106
|
+
"""Applies i.i.d. depolarizing noise at rate `p` to all 3 qubits of
|
|
107
|
+
`sim`, matching the paper's own `D^{\\otimes N}(GHZ_N)`
|
|
108
|
+
honest-implementation model (Eq. A.64). A no-op at `p=0.0`."""
|
|
109
|
+
if p == 0.0:
|
|
110
|
+
return
|
|
111
|
+
sv = np.asarray(sim.get_statevector())
|
|
112
|
+
sv_noisy = NoiseModel.apply_to_sv(sv, 3, "depolarizing", p, rng=rng)
|
|
113
|
+
sim.set_initial_state(np.asarray(sv_noisy))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_round(rng: np.random.Generator, p_dep: float = 0.0) -> int:
|
|
117
|
+
"""Runs one Parity-CHSH test round (Definition 8): Alice and Bob1 get
|
|
118
|
+
uniformly random questions `x, y`; the other Bob gets a FIXED
|
|
119
|
+
question, always 1 (X measurement) -- per the paper's own definition
|
|
120
|
+
text, not 0 (see module docstring for the bug this fixed).
|
|
121
|
+
|
|
122
|
+
Returns 1 if the parties win the round (`(a + b1) mod 2 ==
|
|
123
|
+
(x*(y XOR b2)) mod 2`), else 0."""
|
|
124
|
+
sim = prepare_ghz3()
|
|
125
|
+
apply_depolarizing(sim, p_dep, rng)
|
|
126
|
+
x = int(rng.integers(0, 2))
|
|
127
|
+
y = int(rng.integers(0, 2))
|
|
128
|
+
a = measure_alice(sim, x, rng)
|
|
129
|
+
b1 = measure_bob1(sim, y, rng)
|
|
130
|
+
b2 = measure_other_bob(sim, 1, rng)
|
|
131
|
+
win = (a + b1) % 2 == (x * (y ^ b2)) % 2
|
|
132
|
+
return int(win)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def key_round(rng: np.random.Generator, p_dep: float = 0.0):
|
|
136
|
+
"""Runs one key-generation round: everyone measures Z (`x=0, y=2,
|
|
137
|
+
y2=0`). Returns `(a, b1, b2)`, perfectly correlated in the noiseless
|
|
138
|
+
case."""
|
|
139
|
+
sim = prepare_ghz3()
|
|
140
|
+
apply_depolarizing(sim, p_dep, rng)
|
|
141
|
+
a = measure_alice(sim, 0, rng)
|
|
142
|
+
b1 = measure_bob1(sim, 2, rng)
|
|
143
|
+
b2 = measure_other_bob(sim, 0, rng)
|
|
144
|
+
return a, b1, b2
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parity_chsh_win_rate(n_rounds: int, p_dep: float = 0.0, seed=None) -> float:
|
|
148
|
+
"""Runs `n_rounds` Parity-CHSH test rounds and returns the observed
|
|
149
|
+
winning frequency.
|
|
150
|
+
|
|
151
|
+
Ground truth, verified to machine precision at `p_dep=0.0`:
|
|
152
|
+
`P_win = 0.85355...` (`1/2 + 1/(2 sqrt2)`, the quantum maximum), well
|
|
153
|
+
above the classical bound `3/4` -- device independence requires
|
|
154
|
+
exceeding this classical bound.
|
|
155
|
+
|
|
156
|
+
Example
|
|
157
|
+
-------
|
|
158
|
+
>>> import dense_evolution.protocols.di_qkd_ghz as ghz
|
|
159
|
+
>>> p_win = ghz.parity_chsh_win_rate(3000, p_dep=0.0, seed=0)
|
|
160
|
+
>>> p_win > 0.75
|
|
161
|
+
True
|
|
162
|
+
"""
|
|
163
|
+
rng = np.random.default_rng(seed)
|
|
164
|
+
wins = sum(test_round(rng, p_dep) for _ in range(n_rounds))
|
|
165
|
+
return wins / n_rounds
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def key_qber(n_rounds: int, p_dep: float = 0.0, seed=None):
|
|
169
|
+
"""Runs `n_rounds` key-generation rounds and returns the raw QBER
|
|
170
|
+
between Alice and each Bob, `(qber_b1, qber_b2)`, before error
|
|
171
|
+
correction or privacy amplification. Zero at `p_dep=0.0`, rising with
|
|
172
|
+
`p_dep`."""
|
|
173
|
+
rng = np.random.default_rng(seed)
|
|
174
|
+
disagree_b1 = 0
|
|
175
|
+
disagree_b2 = 0
|
|
176
|
+
for _ in range(n_rounds):
|
|
177
|
+
a, b1, b2 = key_round(rng, p_dep)
|
|
178
|
+
disagree_b1 += a != b1
|
|
179
|
+
disagree_b2 += a != b2
|
|
180
|
+
return disagree_b1 / n_rounds, disagree_b2 / n_rounds
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def expected_win_rate(p_dep: float, n_parties: int = 3) -> float:
|
|
184
|
+
"""Closed-form honest-implementation Parity-CHSH win rate (paper Eq.
|
|
185
|
+
A.65, specialized to `n_parties=3`), written in terms of the
|
|
186
|
+
per-qubit Pauli-expectation shrink factor `s` a depolarizing channel
|
|
187
|
+
produces: `p_exp = 1/2 + s^3/(2 sqrt2) + s^2*(1-s)/(4 sqrt2)`.
|
|
188
|
+
|
|
189
|
+
`s = 1 - 4*p_dep/3`, matching `NoiseModel`'s real isotropic-Pauli
|
|
190
|
+
depolarizing Kraus map (`K0=sqrt(1-p)I, K1..3=sqrt(p/3)*Pauli`), NOT
|
|
191
|
+
`s=1-p` (which would assume a "replace with the maximally mixed
|
|
192
|
+
state" channel instead). Verified directly against the simulator: the
|
|
193
|
+
`s=1-p` version diverged with growing significance as `p` grew
|
|
194
|
+
(`z=-2.75` at `p=0.10`, `z=-3.65` at `p=0.15`, N=3000); `s=1-4p/3`
|
|
195
|
+
matches the simulator across the whole sweep (`|z|<0.5` everywhere
|
|
196
|
+
tested)."""
|
|
197
|
+
s = 1 - 4 * p_dep / 3
|
|
198
|
+
s2 = np.sqrt(2)
|
|
199
|
+
return 0.5 + s ** n_parties / (2 * s2) + s ** 2 * (1 - s ** (n_parties - 2)) / (4 * s2)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""The full multi-round DICKA structure from Ribeiro, Murta & Wehner 2018
|
|
2
|
+
(arXiv:1708.00798), Appendix Protocol 2 -- "a more detailed version of
|
|
3
|
+
Protocol 1". Builds directly on `di_qkd_ghz`'s already-validated
|
|
4
|
+
single-round primitives (GHZ(3) preparation, measurements, depolarizing
|
|
5
|
+
noise, the Parity-CHSH game, the all-Z key round); this module adds the
|
|
6
|
+
round-selection and parameter-estimation layer around them.
|
|
7
|
+
|
|
8
|
+
Per-round structure (paper's own text, step 1):
|
|
9
|
+
|
|
10
|
+
(a) prepare the shared GHZ(3) state.
|
|
11
|
+
(b) Alice picks `T_i ~ Bernoulli(gamma)` and announces it.
|
|
12
|
+
(c) `T_i=0` -> a key round: everyone measures Z (`x, y, y2 = 0, 2,
|
|
13
|
+
0`). `T_i=1` -> a test round: Alice/Bob1 get random questions,
|
|
14
|
+
the other Bob gets question=1 (Definition 8) -- the Parity-CHSH
|
|
15
|
+
game.
|
|
16
|
+
|
|
17
|
+
Repeated for `n` rounds; the protocol aborts if the observed winning
|
|
18
|
+
frequency on test rounds falls below a threshold `beta` in
|
|
19
|
+
`]3/4, 1/2+1/(2 sqrt2)[`.
|
|
20
|
+
|
|
21
|
+
What this does NOT reproduce, and why: Theorem 4's exact secure key
|
|
22
|
+
length `l` is a function of `f-tilde(beta)`, a bound on the single-round
|
|
23
|
+
von Neumann entropy defined in the paper's own Lemma 3 as the unique
|
|
24
|
+
tangent to a convex function at an optimized point `p_opt` -- not a
|
|
25
|
+
closed-form expression, but the output of a separate numerical
|
|
26
|
+
(SDP-style) optimization the paper carries out on its own. Inventing a
|
|
27
|
+
number for it here would mean reporting something the paper never
|
|
28
|
+
actually gives in closed form. What IS reproduced, against the real
|
|
29
|
+
simulator, is everything Protocol 2 actually specifies as a physical
|
|
30
|
+
procedure: the round selection, the honest-implementation winning
|
|
31
|
+
frequency (parameter estimation), the abort decision, and the raw key's
|
|
32
|
+
QBER before error correction -- i.e. everything up to, but not
|
|
33
|
+
including, the privacy-amplification key-rate number."""
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from dense_evolution.protocols.di_qkd_ghz import (
|
|
38
|
+
prepare_ghz3,
|
|
39
|
+
measure_alice,
|
|
40
|
+
measure_bob1,
|
|
41
|
+
measure_other_bob,
|
|
42
|
+
apply_depolarizing,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
CLASSICAL_BOUND = 0.75
|
|
46
|
+
QUANTUM_MAX = 0.5 + 1 / (2 * np.sqrt(2))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_round(rng: np.random.Generator, gamma: float, p_dep: float = 0.0):
|
|
50
|
+
"""Runs one round of Protocol 2, step 1(a)-(c): prepares the shared
|
|
51
|
+
GHZ(3) state, applies depolarizing noise at rate `p_dep`, then with
|
|
52
|
+
probability `gamma` runs a Parity-CHSH test round, otherwise a
|
|
53
|
+
key-generation round.
|
|
54
|
+
|
|
55
|
+
Returns `("test", win)` or `("key", a, b1, b2)`."""
|
|
56
|
+
sim = prepare_ghz3()
|
|
57
|
+
apply_depolarizing(sim, p_dep, rng)
|
|
58
|
+
if rng.random() < gamma:
|
|
59
|
+
x = int(rng.integers(0, 2))
|
|
60
|
+
y = int(rng.integers(0, 2))
|
|
61
|
+
a = measure_alice(sim, x, rng)
|
|
62
|
+
b1 = measure_bob1(sim, y, rng)
|
|
63
|
+
b2 = measure_other_bob(sim, 1, rng)
|
|
64
|
+
win = int((a + b1) % 2 == (x * (y ^ b2)) % 2)
|
|
65
|
+
return ("test", win)
|
|
66
|
+
a = measure_alice(sim, 0, rng)
|
|
67
|
+
b1 = measure_bob1(sim, 2, rng)
|
|
68
|
+
b2 = measure_other_bob(sim, 0, rng)
|
|
69
|
+
return ("key", a, b1, b2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_protocol(n_rounds: int, gamma: float, beta: float, p_dep: float = 0.0, seed=None) -> dict:
|
|
73
|
+
"""Runs the full multi-round DICKA structure for `n_rounds`, then
|
|
74
|
+
applies the abort rule from step 1: abort if the observed test-round
|
|
75
|
+
win frequency `p_hat` is below `beta`.
|
|
76
|
+
|
|
77
|
+
`beta` must sit strictly between `CLASSICAL_BOUND` (0.75) and
|
|
78
|
+
`QUANTUM_MAX` (`0.5 + 1/(2 sqrt2) ~= 0.8536`) to meaningfully
|
|
79
|
+
distinguish a device-independent channel from a classically-bound
|
|
80
|
+
one.
|
|
81
|
+
|
|
82
|
+
Returns a dict with everything the protocol itself produces --
|
|
83
|
+
`n_rounds`, `n_test`, `n_key`, `p_hat`, `beta`, `aborted`, `qber_b1`,
|
|
84
|
+
`qber_b2` -- not the privacy-amplified final key length (see module
|
|
85
|
+
docstring for why).
|
|
86
|
+
|
|
87
|
+
Example
|
|
88
|
+
-------
|
|
89
|
+
>>> import dense_evolution.protocols.dicka_protocol2 as dicka
|
|
90
|
+
>>> beta = (dicka.CLASSICAL_BOUND + dicka.QUANTUM_MAX) / 2
|
|
91
|
+
>>> result = dicka.run_protocol(1000, gamma=0.7, beta=beta, p_dep=0.0, seed=0)
|
|
92
|
+
>>> bool(result["aborted"])
|
|
93
|
+
False
|
|
94
|
+
"""
|
|
95
|
+
rng = np.random.default_rng(seed)
|
|
96
|
+
test_wins = test_total = 0
|
|
97
|
+
key_a, key_b1, key_b2 = [], [], []
|
|
98
|
+
for _ in range(n_rounds):
|
|
99
|
+
outcome = run_round(rng, gamma, p_dep)
|
|
100
|
+
if outcome[0] == "test":
|
|
101
|
+
test_total += 1
|
|
102
|
+
test_wins += outcome[1]
|
|
103
|
+
else:
|
|
104
|
+
_, a, b1, b2 = outcome
|
|
105
|
+
key_a.append(a)
|
|
106
|
+
key_b1.append(b1)
|
|
107
|
+
key_b2.append(b2)
|
|
108
|
+
|
|
109
|
+
p_hat = test_wins / test_total if test_total else 0.0
|
|
110
|
+
aborted = p_hat < beta
|
|
111
|
+
key_a, key_b1, key_b2 = map(np.array, (key_a, key_b1, key_b2))
|
|
112
|
+
qber_b1 = float(np.mean(key_a != key_b1)) if len(key_a) else 0.0
|
|
113
|
+
qber_b2 = float(np.mean(key_a != key_b2)) if len(key_a) else 0.0
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"n_rounds": n_rounds,
|
|
117
|
+
"n_test": test_total,
|
|
118
|
+
"n_key": len(key_a),
|
|
119
|
+
"p_hat": p_hat,
|
|
120
|
+
"beta": beta,
|
|
121
|
+
"aborted": aborted,
|
|
122
|
+
"qber_b1": qber_b1,
|
|
123
|
+
"qber_b2": qber_b2,
|
|
124
|
+
}
|