Swap and network topologies
Alice and Bob both send; an untrusted station between them measures and announces, and the announcement correlates the senders. Neither detects anything.
| Bought | detector side channels leave the threat model: the proof never trusts the relay, so a relay detector has no trusted= flag (the split) |
| Paid | a two-arm channel and a harsher loss budget: relay position matters, and the symmetric position is not the optimum |
Why it is called a swap
Preparing and sending a state is equivalent to measuring half an entangled pair, so this topology is an EPR source per end with a Bell measurement between them: entanglement swapping, the time reversal of BBM92. Lo, Curty and Qi introduced it so, on the time-reversed EPR idea of Biham, Huttner and Mor and of Inamori.
| Why the middle may be untrusted | a Bell measurement projects onto a joint property and reveals nothing about either input |
| Why not "relay" | no photon travels from Alice to Bob. Both terminate at the station, the joint measurement consumes them, and only a classical announcement leaves |
| Caveat | no entanglement exists anywhere: Alice and Bob prepare Gaussian-modulated coherent states. Swapping is the virtual protocol of the security proof; the hardware mixes the arrivals on a balanced beamsplitter and homodynes conjugate quadratures |
q.Swap
Swap resolves components into a plan, refuses what it cannot compute, and returns a SwapResult. The qubit midpoint, q.BellAnalyser with q.TestBasisBound: MDI-BB84.
import qkd as q
res = q.Swap(
alice=q.Sender(modulation=q.GaussianModulation(v_a=1e5)),
bob=q.Sender(modulation=q.GaussianModulation(v_a=1e5)),
relay=q.Relay(bell=q.BellDetector(eta=1.0, v_el=0.0)),
channels=(q.Channel(T=1.0), q.Channel(T=0.1)),
security=q.Asymptotic(beta=1.0),
).run()
res.key_rate # 0.077336 bits per relay use
res.chi # 22.0 equivalent noiserun() signature | no symbols, no seed: a closed form over the configured hardware |
res.key_rate takes no res.attack evaluates the post-relay covariance at the configured one, and past | |
| Where the cap raises | at run(): GaussianModulation is shared with the point-to-point path, which has no cap |
q.Sender(modulation=q.GaussianModulation(v_a=1e6)) # constructs fine
# ... Swap(...).run()
# ValueError: va must be <= 1e5, got 1000000: the conditional blocks cancel
# catastrophically and the rate exceeds its asymptotic bound. Use
# cvmdi_rate for the large-modulation limitThe component tree
| Parameter | Unit | Default | Description |
|---|---|---|---|
Swap.alice, .bob | — | required | Two Senders. Both send; neither measures. The rate is not symmetric under exchanging them. |
Swap.relay | — | required | The untrusted Relay. |
Swap.channels | — | required | (alice's arm, bob's arm), each a Channel or Fiber. Exactly two. |
Swap.security | — | None | On a q.BellDetector midpoint, Asymptotic or q.TwoModeBound(block=q.GaussianBlock(…)); on a q.BellAnalyser one, q.TestBasisBound. None always fails at run(): no security model is safe to fall back to. |
Swap.environment | — | None | A CorrelatedEnvironment. None is the independent pair. |
Sender.modulation | — | required | GaussianModulation beside a BellDetector; a basis- or polarisation-keyed modulation beside a BellAnalyser. |
Sender.pulse | s | None | Intensity FWHM of this sender's temporal mode. Refused on the covariance path, which has no two-source mode model. Beside a BellAnalyser, widths on both senders derive misalign_test; one width alone, or widths beside a pinned misalign_test, is refused. |
Relay.bell | — | required | A BellDetector or a BellAnalyser. |
BellDetector.eta | — | required | Relay detection efficiency |
BellDetector.v_el | SNU | required | Relay electronic noise |
CorrelatedEnvironment.x, .p | SNU | required | Eve's cross-arm environment correlations |
What run() returns
| Field | Description |
|---|---|
key_rate | The bound, bits per relay use: the worst attack compatible with the observed |
chi | The equivalent noise the two arms carry. |
floor | What two pure-loss arms would carry; see below. |
least | The domain bound, where the noise term vanishes. |
attack | Attack(i_ab, chi_e, key_rate) at the configured finite |
explain | The labelled plan, with no stages key: a relay run has one stage. |
Only SwapResult.key_rate is clamped. explain()["key_bound"] and ["key_raw"] carry the unclamped bound; Attack.key_rate (explain()["key_attack"]) is never clamped.
The continuous-variable relay
Each sender sends a Gaussian-modulated coherent state down its own thermal-loss arm, transmissivity
| Implementation | two thermal-loss channels, a beamsplitter and two homodynes, conditioned through the Schur complement of State.condition; no bespoke relay formula |
| Roles | the encoder's variable becomes the key; the decoder combines his variable with the announcement to reconstruct it. Reconciliation direction is not symmetric |
Everything depends on one number
In the large-modulation limit the rate is a closed form in the two transmissivities and one equivalent noise
| Excess noise enters as | each arm's environment variance |
res.floor | |
res.least | |
| Relay imperfections | folded into the two arms: |
| Why the fold is exact | equal loss on every mode commutes with any passive linear network, so detector efficiency after the balanced coupler is a loss on both arms before it |
Relay position is the result
Total transmittance fixed at res.attack.key_rate (res.key_rate is
| Relay position | Rate, bits per relay use | |
|---|---|---|
| at the decoder | ||
| symmetric | ||
| at the encoder |
The rate climbs monotonically and crosses zero only at the last step: the symmetric relay is
Zero crossings against Pirandola et al., Nat. Photon. 9, 397 (2015), arXiv:1312.4104, are graded at Validation. With the decoder's arm lossless the rate is
What res.attack is, and is not
res.key_rate — cvmdi_rate | res.attack — cvmdi_point | |
|---|---|---|
| Is | a security bound: the worst attack compatible with the observed | one specified attack: the two environment variances and the correlations |
| Modulation | the large-modulation limit; takes no | the finite |
| Reconciliation | the Shannon limit, so the configured | the configured |
| Returns | a rate |
| Why unclamped | most of the |
| Join | raising the modulation drives the point onto the asymptote, and is tested |
attack.key_rate above key_rate | possible: cvmdi_rate reads the observed noise as the worst attack producing it, cvmdi_point prices one named Eve at finite test_relay_bounded finds neither rate above capacity in any of the 4,518 configurations the engine accepts out of 40,000 drawn, closest approach 0.41, and a 200,000-draw search maximising b92_ceiling and cow_rate escape PLOB, pinned in test/capacity.py's Escapes; neither is CV-MDI |
| Wrong sign | fights the correlations the Bell measurement establishes and costs more key than two independent entangling cloners, so the equivalent noise takes |
| PLOB | every pure-loss configuration sits strictly under |
Where it is not
Asymmetric modulation and a symbol-level relay: roadmap.
Twin-field, phase-matching and sending-or-not-sending: permanent exclusion.
Graded numbers: Validation.
q.Network
A graph of key-holding nodes. Every vertex is a q.Node holding key in the clear; every edge is a q.Hop carrying one q.Link or q.Swap.
import qkd as q
net = q.Network(
nodes={"IITM": q.Node(), "ERNET": q.Node(), "NIC": q.Node()},
edges=[
q.Hop(ends=("IITM", "ERNET"), link=cow, clock=1e9),
q.Hop(ends=("ERNET", "NIC"), link=dps, clock=1e9),
],
)
res = net.run()
res.rates[("IITM", "ERNET")] # bits per second
res.rates[("ERNET", "IITM")] # the same; pairs read either way round
route = res.route("IITM", "NIC")
route.hops # (("IITM", "ERNET"), ("ERNET", "NIC"))
route.trusts # ("ERNET",)
route.bottleneck # bits per second, the minimum over the hops
route.security # "trusted-node key relay through ERNET; a key-management
# throughput, not a quantum bound"An untrusted station is not a vertex
A q.Swap is one edge. Its untrusted midpoint is interior to that edge and in no vertex set, segment key or route: the data structure cannot express it.
| Why | untrusted relays do not compose: chaining two swaps needs quantum memory or a repeater |
| What it buys | detector-side-channel immunity for one segment, at a worse loss budget. No added reach |
| What it costs in the model | two swap edges through one building each carry their own q.Relay; one detector pair serving two segments is not expressible |
| Consequence for routing | a swap segment is one edge between its senders; the router sees neither arm and no name to route through |
There is no end-to-end key rate
Neither NetworkResult nor Route has one; asking raises with the reason:
res.key_rate
# AttributeError: a network has no key rate. Per-segment rates are in
# res.segments and res.rates; a path number is res.route(a, b).bottleneck,
# which is a KEY-MANAGEMENT THROUGHPUT and not a security bound, because no
# quantum bound spans a trusted noderoute.bottleneck is the minimum hop rate in bits per second: hop-by-hop key relay spends one local key bit per relayed bit. route.trusts names every intermediate node, each of which held the end-to-end key in the clear. It is empty only on a single-hop route, the one case where the number is a quantum claim.
The component tree
| Parameter | Unit | Default | Description |
|---|---|---|---|
Network.nodes | — | required | {name: q.Node}; the key is the name. A q.Relay is refused: an untrusted station holds no key |
Network.edges | — | required | A list of q.Hop, not a dict keyed by pair, so no parallel clocks= mapping can disagree |
Hop.ends | — | required | Two distinct node names. (transmitter, receiver) for a q.Link; (alice, bob) for a q.Swap, following its encoder convention |
Hop.link | — | required | A q.Link or a q.Swap, verbatim, run and multiplied by the clock: a network reports what the standalone payload reports |
Hop.clock | Hz | required | Symbols per second on the quadrature families, emitted pulses per second on the click ones |
Node | — | — | Carries nothing; physical descriptions live on the edge |
The clock is mandatory. LinkResult.key_rate is bits per symbol on Gaussian modulation and SwapResult.key_rate is per swap use. A minimum over a mixed path would mix three units. It cannot come from q.Alice.symbol_rate: q.Link refuses an Alice on the intensity-keyed path.
What Network.run() returns
| Field | Description |
|---|---|
segments | {(a, b): LinkResult | SwapResult}, per-shot rates in each payload's own unit. Reads either way round |
rates | {(a, b): bits per second}, the only unit that compares across families |
nodes | {name: NodeReport} with degree, segments and total, the sum of the incident segments in bits per second |
explain | the labelled plan, with trust, units, end_to_end and contention rows always present |
run(symbols=…, seed=…) reaches the q.Link payloads alone; a q.Swap is a closed form and takes neither.
Routing
res.route(a, b) returns the widest path — largest bottleneck, ties broken by fewest hops and so fewest trusted nodes — key relay being limited by its narrowest hop.
| Refused | |
|---|---|
| An unknown endpoint | names the nodes the network does have |
| A route from a node to itself | a route joins two distinct nodes |
| A disconnected pair | says so, noting that an untrusted swap station is never a vertex |
Contention is not modelled. A node relaying two concurrent routes splits its key pool, so two route() calls on one result cannot both be achieved.
What the graph layer does not model
| Absent | |
|---|---|
| Authentication key consumption | hop-by-hop relay spends key authenticating every round, so delivered key is strictly below bottleneck |
| Key pool dynamics | no buffering, scheduling, refill or contention |
| The relay operation itself | the XOR of local keys, its failure modes, its latency |
| Time | no availability, no outage, no drift |
| Parallel segments between one pair | refused: their key pools would add and nothing here tracks that |
| Point to multipoint | a receiver shared by several transmitters; hardware lives on the edge |
| Wavelength | no component carries one, so per-edge Raman crosstalk is not derived from a channel plan |
| Multi-path key splitting | route() returns one path |
| The control plane | key management APIs, SDN control, and everything above the physical layer |