qkd
A QKD protocol simulator with the hardware in the loop, over a native Rust core.
Documentation: https://plutoniumm.github.io/qkd/
What it is
Most CV toolkits let you set the channel transmittance T and the excess
noise ξ, then compute a key rate from the same numbers you set. That is the
textbook derivation, not a protocol simulation.
qkd inverts it. Excess noise is an output, assembled from parameters you
can measure on a bench — laser linewidth, detector electronic noise, quantum
efficiency, DAC and ADC bit depth, pilot power ratio, symbol rate, fibre length
— with the DSP chain in the loop: pilot-assisted phase recovery, carrier-offset
estimation, frame rotation. The residual phase error those estimators leave
behind feeds back into the noise budget as ξ_phase. Parameter estimation then
works from finite samples with confidence bounds, the way Alice and Bob have to.
import qkd as q
link = q.Link(
modulation=q.GaussianModulation(v_a=4.0),
channel=q.Fiber(length=15.0, alpha=0.2), # hardware, not T
alice=q.Alice(
laser=q.Laser(linewidth=10e3),
pilots=q.Pilots(power_db=12.0, freq=180e6),
symbol_rate=100e6,
),
bob=q.Bob(
detector=q.Heterodyne(eta=0.6, v_el=0.1, trusted=True),
lo=q.LocalLO(linewidth=10e3),
),
dsp=q.DSP(cfo=q.PilotCFO(), phase=q.PilotPhase(), block=32),
security=q.FiniteSize(beta=0.95, n=1e9),
)
res = link.run(symbols=200_000, seed=7)
res.dsp.v_err # 0.0075742 rad^2 left by the pilot estimator
res.budget.total # 0.0304118 SNU at the channel input -- derived from it
res.est.xi # 0.0142987 what Alice and Bob estimate from the samples
res.key_rate # 0.0285553 bit/symbol
No line above sets ξ. It is what the linewidth, the pilot ratio, the symbol
rate and the recovered phase produce.
Two rules follow the thesis everywhere. Every noise quantity names its plane
— ξ at the channel input and at Bob's side differ by a factor of T, so the
API refuses to guess which you meant. And trusted versus untrusted detector
noise is an explicit flag, because it is a security-model decision, not a
physics one.
Underneath sits an ordinary Gaussian CV simulator (symplectic covariance engine, thermal-loss channels, homodyne/heterodyne detection) which is useful on its own, with a truncated-Fock layer beside it for the states a covariance matrix cannot carry.
Status
0.1.0. Never published. pip install qkd does not work and will 404 —
the wheel pipeline runs locally and nothing has been uploaded. Build from source
(below).
qkd today is 26 Rust files (18,035 lines) plus 6 WGSL shaders (472 lines)
under src/, 9,196 lines of Python under qkd/, and 49 test scripts building
1,444 exams across 241 Exams. qkd.__all__ lists 82 public names and is the
authoritative API index; qkd._core exposes 199 — 192 functions and 7 classes.
Counted 2026-09-04, and stale the moment an engine lands.
Configurations the library cannot compute raise, naming the restriction, rather than approximating. How well each protocol is checked is graded rather than asserted — see Validation, which scores every protocol on two axes: what the theory reproduces (the same number, a correctly signed inequality, or behaviour only) and, separately, whether a published experiment was reproduced.
Protocols
Discrete and continuous variable, prepare-and-measure and entanglement-based. Every row runs end to end and is covered by exams.
| Reached through | Protocol |
|---|---|
q.Link + q.GaussianModulation |
Gaussian-modulation CV-QKD: homodyne and heterodyne, trusted and untrusted, asymptotic and finite-size, pilot DSP in the loop |
q.Link + q.PhaseShiftKeying |
M-PSK discrete modulation |
q.Link + q.BasisKeying |
BB84 with weak coherent pulses and decoy states; bases=3 is six-state, announce="pair" is SARG04 |
q.Link + q.PolarisationKeying |
the same, with the polarisation reference frame named |
q.Link + q.TwoStateKeying |
B92; reference=True selects the strong-reference variant |
q.Link + q.DifferentialPhase |
DPS, with the QBER derived from the delay interferometer |
q.Link + q.IntensityKeying |
COW |
q.PairLink + q.SymmetryBound |
BBM92 over a photon-pair source |
q.PairLink + q.ViolationBound |
E91, priced by an observed CHSH violation instead of a phase error |
q.Swap + q.BellDetector |
CV-MDI, the untrusted-relay topology |
q.Swap + q.BellAnalyser |
MDI-BB84, the same topology with a counting midpoint. Asymptotic |
q.Network |
the graph layer: trusted nodes, q.Hop edges, widest-path routing |
Four more engines ship with exams but no q.* wrapper yet, and are reached on
qkd._core directly: round-robin differential phase shift (rrdps_*),
mode-pairing / asynchronous MDI (pairing_*), a hand-rolled semidefinite phase
bound for COW′ (sdp_*), and the loss-tolerant analysis of source flaws
(flaw_*). So does the Winick–Lütkenhaus relative-entropy proof for discrete
modulation (dm_secure, on the complex-Hermitian numerics herm_*) — a second
solver, on a different cone from sdp_*.
Three exported components describe hardware and are refused by q.Link rather
than approximated: q.ThresholdArray and q.PnrDetector build a
photon-number-resolving POVM no shipped bound can read, and q.TransmittedLO
models the oscillator that travels with the signal, where every q.Link runs a
locally generated one. Each refusal names what is missing.
Two Python modules sit beside the protocols. qkd.attacks holds the
detector-side attacks — saturation, calibration, blinding, time-shift, blanking
— reporting what Alice and Bob observe and what Eve holds as separate books
of numbers, and refusing to be asked for a key rate, because under every attack
modelled there the observables stay at values an unattacked link would produce.
qkd.reconcile is the classical layer: LDPC and Cascade reconciliation,
privacy amplification, authentication cost.
Four exclusions are settled decisions rather than pending work: MDI-DPS (patented), twin-field / phase-matching / sending-or-not-sending, satellite and free-space channels, and qudits.
Layers
| Module | What |
|---|---|
qkd.gaussian |
symplectic covariance engine, thermal-loss channels, homodyne/heterodyne, Wigner and Husimi grids |
qkd.fock |
truncated number basis: cat, GKP, squeezed, explicit density matrices, and the Wigner-negativity witness |
qkd.budget |
ξ assembled from hardware, referred between four named planes |
qkd.impairments |
hardware descriptors and the closed forms behind them |
qkd.pairs |
photon-pair coincidence gains, BBM92 and CHSH |
qkd.attacks |
detector-side attacks, as observables against Eve's holdings |
qkd.reconcile |
error correction, privacy amplification, authentication |
Build from source
git clone https://github.com/plutoniumm/qkd
cd qkd
./do develop # compiles the Rust core in place -- this is the import path
You need Python ≥ 3.11 with numpy, plus a Rust toolchain (cargo, rustc).
./do develop runs setup.py build_ext --inplace and installs its own build
dependencies. numpy is a hard requirement, not an optional extra — every bulk
numeric return crosses the native boundary as an ndarray.
A bare cargo build will fail to link. A PyO3 extension-module cdylib has
no Python symbols to resolve against; setup.py build_ext supplies
-undefined dynamic_lookup. That failure is expected. Always go through
./do develop.
./do build produces cp311-abi3 wheels for macOS (arm64, x86_64), Linux
manylinux_2_28 (aarch64, x86_64) and Windows x86_64, plus an sdist. It has
been run; ./do deploy has not.
Compute backends
The core resolves a backend at runtime and reports it:
>>> import qkd
>>> qkd.backend_info()
('gpu', 'f32', 'Apple M2 (Metal)')
That tuple is machine-specific; ('cpu', 'f64', ...) on a machine with no
adapter is a correct result, not a failure.
GPU support is on by default, via wgpu (Metal / Vulkan / DX12). It needs no
vendor SDK at build time, which is what keeps the local cross-compile matrix
working from a single Apple Silicon machine — a CUDA dependency would break it
outright and could never be tested here.
Dispatch asks for a precision, not a device. WGSL has no f64, so the GPU
path is single-precision, permanently. That suits the DSP layer, which runs over
ADC samples carrying 12–16 real bits, where f32's 24-bit mantissa is already
past the hardware's resolution. Covariance matrices, symplectic maps and the
Holevo bound are 2×2 and 4×4, negligible to compute and sensitive to
cancellation, and stay in f64 on the CPU whatever hardware is present.
No q.Link run touches the GPU. The WGSL kernels exist, dispatch, and are
measured — Threefry is bit-exact against the Rust past counters of 2³², and DSP
statistics agree with the f64 CPU path to about 1e-7 — but they compute the
oracle branch, not the DSP-derotated one: the pilot DSP across blocks and the
phase-walk carry scan are sequential and stay on the host, where a monolithic
f32 scan errs 4.0e-2 rad, the same size as the residual phase noise the
simulation exists to measure. run_symbols, and therefore every q.Link run,
is CPU. The measured in-kernel throughput figures (313–335 M symbol/s against
10.3 M symbol/s for the CPU pipeline, crossover at 1e3–3e3 symbols) come from a
machine under heavy load, so the CPU rows are roughly 2× slow and no single
speedup number is claimed.
Development
Everything goes through ./do. There is no Makefile, no pyproject.toml, and
no CI.
./do develop # compile the Rust core in place (debug) -- the import path
./do check # cargo check + python compileall, fast, no link
./do test [name ...] # develop + run test/*.py, emit markdown reports
./do bench [name ...] # release build + bench/*.py through the MDB harness
./do lint [--fix] # view.lint + black --check + eastwood + svelte-check
./do docs # vitepress dev server on :3000
./do build # every wheel -> wheelhouse/, sdist -> dist/ (no upload)
./do deploy # twine check + upload what build produced
./do needs python on PATH; on a machine that has only python3, activate
an environment first. ./do test and ./do bench take file selectors, so
./do test anchors budget re-runs those two alone.
Tests are standalone scripts, not pytest, and emit markdown report tables. To publish them into the docs site:
MDR_OUT="$(pwd)/docs/tests" ./do test
See RELEASING.md for the wheel matrix, how the cross-compiles work, and the three release gates, and docs/architecture.md for the layering.
License
MIT
Release files for qkd 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qkd-0.1.0.tar.gz | 522.3 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| qkd-0.1.0-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details |
| qkd-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| qkd-0.1.0-cp311-abi3-manylinux_2_28_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| qkd-0.1.0-cp311-abi3-macosx_11_0_x86_64.whl | CPython 3.11 | abi3 | macOS 11.0+ x86-64 | Details |
| qkd-0.1.0-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 14.7 MB
Release files / qkd-0.1.0.tar.gz
| Download URL | qkd-0.1.0.tar.gz |
|---|---|
| Size | 522.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e0ccbf2775014c89fc60bbe61ad3775b2f65cad5106d02250621914d70ef3b45
|
|
BLAKE2b-256 checksum How to use checksums |
4fc86798ca9bcc40791d38c17dd887a0e45f36d34aff05403da43a6b2f2374bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qkd-0.1.0-cp311-abi3-win_amd64.whl
| Download URL | qkd-0.1.0-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 3.1 MB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
602bc633246a043b699f302891d833da45eb3d7476d1b3cb11ff0358c028b226
|
|
BLAKE2b-256 checksum How to use checksums |
af487ebf1d13a2e69802dee4613aabf96fdaebbf75a339c5cd15fe50b1701c1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qkd-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl
| Download URL | qkd-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.9 MB |
| Tags | CPython 3.11 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
27dbf18017ed97e62a63b0af7d9f8daa864c0e5f232725a7e4c763bb56d113da
|
|
BLAKE2b-256 checksum How to use checksums |
0c7bc0d6c899df793219846d3643ea87bd99737d56ac32d773e79aa26cc4c428
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qkd-0.1.0-cp311-abi3-manylinux_2_28_aarch64.whl
| Download URL | qkd-0.1.0-cp311-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.8 MB |
| Tags | CPython 3.11 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
36fdaba1c71b59e0019f5e4a5491ec05f0a7bdaef1ea6ca4a36bbfb9db634b5b
|
|
BLAKE2b-256 checksum How to use checksums |
49d58bf3fc8dfc88988ab20b10d165e193abd4017ec664ad8d3c81d468c0ef31
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qkd-0.1.0-cp311-abi3-macosx_11_0_x86_64.whl
| Download URL | qkd-0.1.0-cp311-abi3-macosx_11_0_x86_64.whl |
|---|---|
| Size | 2.8 MB |
| Tags | CPython 3.11 abi3 macOS 11.0+ x86-64 |
|
SHA-256 checksum How to use checksums |
c3800e0cdce29cb78eac67deb9e969a819cd90c31ed0aab2d6e2cba36d8e9587
|
|
BLAKE2b-256 checksum How to use checksums |
dc040c064c54f7e1e28355f750241175f9ee2c9ba5adc1628187448ff87e3b4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qkd-0.1.0-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | qkd-0.1.0-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9de88031ea7353ccf8f14dd12abb604ece2600b1c9865474f4ec6efc59ec6532
|
|
BLAKE2b-256 checksum How to use checksums |
928cc591894ba031858ab17d70c4aabaf5fd4e5fa9fc31ca4ad5a15e1b6303e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|