Noise budget
qkd.budget assembles q.budget.
from qkd import budgetAssembling a budget
bud = budget.assemble(
v_a=5.0, # SNU
t=10**-0.5, # 25 km at 0.2 dB/km
v_err=2e-3, # residual phase-error variance, rad^2
rin=-155.0, # dBc/Hz
bandwidth=100e6, # Hz
dac_bits=16,
adc_bits=12,
)
print(bud.table())source xi @ input xi @ bob plane
phase 1.0010e-02 3.1654e-03 Kish (80)-(82) inferred channel: input-referred, no /T
dac 9.5749e-10 3.0279e-10 Laudenbach (9.37): stated Bob-plane, /T applied
adc 3.1414e-06 9.9341e-07 Laudenbach (9.108): stated Bob-plane, /T applied
rin_sig 8.8914e-04 2.8117e-04 Laudenbach (9.9): stated Bob-plane, /T applied
rin_lo 6.4528e-08 2.0406e-08 Laudenbach (9.21): stated Bob-plane, /T applied
total 1.0902e-02 3.4476e-03phase row has two published forms.
A parameter left out omits its row rather than reporting a zero:
budget.assemble(v_a=5.0, t=0.3, v_err=2e-3).table()
# source xi @ input xi @ bob plane
# phase 1.0010e-02 3.0030e-03 Kish (80)-(82) inferred channel: input-referred, no /T
# total 1.0010e-02 3.0030e-03assemble(...)
Keyword-only; v_a and t required. Plane is where the source formula was stated, not where the entry is stored: a Bob-plane row is already divided by Entry.plane_note records it.
| Parameter | Unit | Plane | Default | Description |
|---|---|---|---|---|
v_a | SNU | input | required | |
t | — | — | required | Transmittance of the fibre span alone, Budget.T is t times the chain, and that product divides every Bob-plane formula. |
v_err | rad² | input | None | Residual phase-error variance after pilot-assisted recovery (which one). Emits phase, a property of the recovered phase and so not divided by |
xi | SNU | input | 0.0 | Excess noise the channel already carried, renormalised alongside phase and by nothing else. Not a row. |
phase_form | — | — | "estimator" | "estimator" or "literature"; anything else raises. |
rin | dBc/Hz | Bob | None | RIN of signal laser and LO. Emits rin_sig and rin_lo. |
bandwidth | Hz | — | None | Detection bandwidth rin and raman_db. |
raman_db | dBm/nm | Bob | None | Measured spontaneous-Raman spectral density raman. Referred by |
wavelength | m | — | 1550.12e-9 | raman_db alone: |
dac_bits | bit | Bob | None | Alice's DAC resolution dac. |
adc_bits | bit | Bob | None | Bob's ADC resolution adc. |
losses | — | — | () | q.Connector, q.Splice, q.Coupling — see the loss chain. |
mu | — | — | 2.0 | Detector multiplicity inside adc: |
ratio | — | Bob | 10.0 | ADC full scale in shot-noise standard deviations, |
| Input | Result |
|---|---|
negative v_err or xi | raises, not clamped. Both forms of |
xi with no v_err | raises: it reaches the phase row and nothing else |
xi with phase_form="literature" | raises: that form renormalises nothing, so the channel noise would be dropped |
rin or raman_db without bandwidth, or bandwidth with neither | ValueError |
vel= or trusted= | TypeError — see why |
Budget and Entry
Budget is a frozen dataclass, so its entries concatenate with impairments.assemble_extra()'s.
| Member | Type | Description |
|---|---|---|
Budget.entries | tuple[Entry, ...] | One per described source, in emission order. |
Budget.T | float | Channel input to detector input — the span and every itemised optic. |
Budget.launch | float | Transmittance of the optics at Alice's output; 1.0 when none declared. |
Budget.receive | float | Transmittance of the optics at Bob's input; 1.0 when none declared. |
Budget.span | float | |
Budget.losses | tuple[Loss, ...] | The chain in optical order, the fibre's own attenuation among it as fiber_span. |
Budget.total | float | Sum of every entry's |
Budget.factor(plane, eta=None) | float | Factor referring an input-referred noise to that plane. |
Budget.at(plane, eta=None) | dict[str, float] | {source: xi} at one of the four planes. |
Budget.refer(xi, start, end, eta=None) | float | One number moved between two planes — for a |
Budget.table() | str | The aligned text table above. |
Entry.source | str | A row name from the per-source table. |
Entry.xi | float | Input-referred, SNU. |
Entry.plane_note | str | The citation and the plane its source paper stated it in. |
Loss.source | str | fiber_span, connector_launch, splice_span, coupling_receive. |
Loss.db | float | The row's insertion loss in dB, count included. |
Loss.site | str | "launch", "span" or "receive". |
Loss.transmittance | float |
The per-source table
Bob-plane rows are divided by
| Source | Formula | Stated at | Reference |
|---|---|---|---|
phase | phase_form="literature" | input | [K24] App. E, Eqs. (76)–(82), by rearranging (80)–(82); [MA17] Eqs. (10)–(11) for the second, reproduced as [K24] App. F, Eq. (85) |
dac | Bob | [L18] Eq. (9.37) | |
adc | Bob | [L18] Eq. (9.108), quantisation term | |
rin_sig | Bob | [L18] Eq. (9.9) | |
rin_lo | Bob | [L18] Eq. (9.21) | |
raman | Bob | [L18] Eq. (9.63), over Eq. (9.59) |
No detector row and no trusted flag — why.
Two connecting substitutions are qkd's, not verbatim in the sources:
| Row | Substitution |
|---|---|
dac | |
adc | Eq. (9.108)'s opto-electronic prefactor reduced to one ratio |
The raman row
raman_width() supplies raman_width(1e9)
A matched receiver,
assemble still takes bandwidth: raman() as two separate parameters, and either alone moves the answer.
Two conventions, a factor of two apart. [L18] carries no impairments.raman, keeps the half as the LO's polarisation-mode selectivity. The same density reads twice as high here, the conservative side; test_raman_convention pins the ratio at
The formulas individually
Each returns an input-referred
| Function | Arguments | Returns |
|---|---|---|
phase(v_a, v_err, xi=0.0, *, form="estimator") | either form | |
dac(v_a, bits, t) | ||
adc(bits, t, mu=2.0, ratio=10.0) | ||
rin_sig(v_a, rin_db, bandwidth, t) | ||
rin_lo(rin_db, bandwidth, quad_var, t) | RIN in dBc/Hz, | |
raman_width(bandwidth, wavelength=1550.12e-9) | ||
raman(n_ram, width, symbol, t, *, wavelength=1550.12e-9) |
budget.phase(5.0, 2e-3) # 0.010010
budget.phase(5.0, 2e-3, 0.01) # 0.010030
budget.phase(5.0, 2e-3, form="literature") # 0.009995
budget.dac(5.0, 16, 10**-0.5) # 9.575e-10
budget.adc(12, 10**-0.5) # 3.141e-06
budget.raman_width(1e9) # 8.015e-12 (Laudenbach's 8 pm)
budget.raman(-80.0, 8.015e-12, 1e-9, 1.0) # 1.251e-03 at Bob, T = 1assemble supplies rin_lo's
The two forms of the phase term
Which
Both read
[K24] Eq. (25)'s src/pipeline.rs reports both:
| Field | Quantity | Ceiling | Feeds |
|---|---|---|---|
SimOut.v_err | none | the budget | |
SimOut.v_wrap | the wrapped second moment about zero | a phase tracker's own error budget |
Only
Where the estimator form comes from
[K24] is the citation of record: it concerns a phase variance and names the model it corrects.
| Source | Route |
|---|---|
| [K24] App. E, Eqs. (76)–(82) | the channel Alice and Bob infer through a phase-noise channel. The correlation carries |
| [S21] Eqs. (18)–(19) | a deterministic receiver angle: |
| [U12] | the fading-channel form, independently; the passage is unnumbered |
budget.phase returns the increment over
infer(), the transmittance half
[K24] Eq. (78) carries the other half of the rotation,
budget.infer(t, v_err, form="estimator") # -> t * exp(-v_err)in whatever plane t is stated. The halves pin:
so charging the noise half against an unattenuated v_err = inf returns 0.0. form="literature" is refused:
budget.infer(0.5, 0.3, form="literature")
# ValueError: form='literature' has no transmittance half to return:
# 2*V_A*(1 - exp(-v_err/2)) is derived holding the transmittance estimate
# fixed, the assumption Kish App. E names as its error. Use the default
# form='estimator', or carry t as it standsBudget.T | the physical loss chain, unmoved |
Budget.inferred | the same T as parameter estimation infers it, T where no phase row was formed, so it may be read unconditionally; raises where that row is the literature form |
v_err and phase_form are set and cleared together. A q.Link run fills them with the estimator form and carries v_err into the Budget carried rather than charged: T stays the physical chain, no explain["T_claimed"]. A hand-built budget that pins its own T and never reads inferred is the case the identity warns about.
Where the difference comes from
The literature form is src/pipeline.rs). Through the regression,
The renormalisation is the difference, and why
Which plane, and which denominator
The second-order answer depends on plane and denominator — the referral trap at second order.
| Choice | What is shipped |
|---|---|
| Plane | The channel input (Alice), for both |
| Slope | The least-squares |
| Denominator | |
| Receiver | Calibrated inside the estimator: src/pipeline.rs computes them. |
The last row belongs to the estimator, not the security model: a rate from these estimates may still hand trusted=False. Referring to Bob's plane, or dividing by
They agree to first order, then separate
With
The ratio is analytic: with this
| estimator | literature | literature is low by | |
|---|---|---|---|
| 0.005003 | 0.004999 | 0.07% | |
| 0.010010 | 0.009995 | 0.15% | |
| 0.050251 | 0.049875 | 0.75% | |
| 0.256355 | 0.246901 | 3.7% | |
| 0.525855 | 0.487706 | 7.3% | |
| 1.615649 | 1.306418 | 19.1% | |
| 8.591409 | 3.934693 | 54.2% |
Measured against the simulator's own estimator
| measured | literature is low by | estimator is off by | |
|---|---|---|---|
| 0.0024 | 0.0126 | +3.4% | +3.1% |
| 0.0535 | 0.2771 | +5.0% | +0.8% |
| 0.1575 | 0.8557 | +13.0% | +0.3% |
| 0.2715 | 1.5745 | +24.0% | +0.9% |
| 0.3990 | 2.4676 | +36.5% | +0.6% |
| 0.5957 | 4.0817 | +58.5% | +0.2% |
| 1.1513 | 10.9099 | +149.3% | +0.8% |
| 2.0683 | 35.0527 | +443.9% | +1.4% |
| Column | Behaviour |
|---|---|
| literature | monotone, one-signed, following the analytic ratio above |
| estimator | flat: an identity in |
| the run's own slope ratio | reproduces the measurement to better than test_estimator_slope_ratio |
A modelling error, not a security hole: a deployed system measures
The literature form's domain
| Form | Domain | Past it |
|---|---|---|
"literature" | PHASE_LIMIT = 0.1 rad² | saturates at PhaseDomainWarning naming |
"estimator" | none |
The
warnings.simplefilter("error", PhaseDomainWarning) escalates the warning; "ignore" silences it. There is no WRAP_LIMIT: its 1.5 rad² was the wrapped second moment saturating.
Which one to select
budget.assemble(v_a=5.0, t=0.5, v_err=2e-3, xi=0.01) # estimator row
budget.assemble(v_a=5.0, t=0.5, v_err=2e-3, phase_form="literature")| Task | form= | Why |
|---|---|---|
| Predicting a rate, or comparing against anything this simulator measures | "estimator", the default | it is what parameter estimation reports |
| Reproducing a published number computed with the other form | "literature" | [MA17] Eqs. (10)–(11) state |
Entry.plane_note names the expression behind each phase row.
Link with a DSP chain derives Link-level selector (res.explain["phase_form"] names the form): to reproduce a literature-form number on a Link, assemble here and pin the total onto q.Channel(xi=…, ref="input"). impairments.dephasing carries the same selector and default.
The loss chain
import qkd as q
bud = budget.assemble(
v_a=5.0,
t=10**-0.5, # the SPAN alone, 25 km at 0.2 dB/km
v_err=2e-3,
rin=-155.0,
bandwidth=100e6,
dac_bits=16,
adc_bits=12,
losses=(
q.Connector(loss=0.25, count=2, site="launch"),
q.Splice(loss=0.02, count=6, site="span"),
q.Coupling(loss=1.5, site="receive"),
),
)
print(bud.table())loss dB T site
connector_launch 0.5000 0.891251 launch
fiber_span 5.0000 0.316228 span
splice_span 0.1200 0.972747 span
coupling_receive 1.5000 0.707946 receive
link 7.1200 0.194089
source xi @ input xi @ bob plane
phase 1.0010e-02 1.9428e-03 Kish (80)-(82) inferred channel: input-referred, no /T
dac 9.5749e-10 1.8584e-10 Laudenbach (9.37): stated Bob-plane, /T applied
adc 5.1183e-06 9.9341e-07 Laudenbach (9.108): stated Bob-plane, /T applied
rin_sig 8.8914e-04 1.7257e-04 Laudenbach (9.9): stated Bob-plane, /T applied
rin_lo 8.0261e-08 1.5578e-08 Laudenbach (9.21): stated Bob-plane, /T applied
total 1.0904e-02 2.1164e-03| On the run above | |
|---|---|
| 5.0 dB of fibre becomes 7.12 dB of link | each decibel has a named row |
adc moves to | stated at Bob's plane and referred to the input by the total |
phase, dac, rin_sig do not move | Alice-plane quantities |
| a second descriptor of the same kind at the same site | raises rather than merging. count= multiplies the decibels on one line |
What each one is, and what it costs
| Component | What it is physically | Typical | Source |
|---|---|---|---|
q.Connector | A mated pair of ferrules: mode mismatch plus the lateral offset and air gap the sleeve leaves. | 0.25 dB (FC/APC), 0.3 dB (FC/PC) | IEC 61753-1 grade C: mean |
| grades B / C / D, mean (max) | 0.12 (0.25) / 0.25 (0.50) / 0.50 (1.00) dB | IEC 61753-1 attenuation grades, the max holding for $>$97% of samples | |
q.Splice | A fusion splice: core eccentricity and mode-field mismatch. | 0.02 dB core-aligning, 0.03–0.04 dB v-groove | Corning/AFL AN0041, SMF-28 Ultra at 1550 nm, measured per ANSI/TIA/EIA-455-8. Telcordia GR-20-CORE asks for a group mean |
q.Coupling | Light crossing between two different modes — fibre to chip, free space to fibre, fibre to a bulk bench: the overlap integral of two mode profiles. | no default | the five rows below |
| silicon grating coupler | 3.1 dB, or 1.0–1.6 dB with a bottom reflector | Mu et al., Appl. Sci. 10, 1538 (2020), Table 2; Cheng et al., Micromachines 11, 666 (2020) | |
| silicon inverse-taper edge coupler | 1.3–1.5 dB | Mu et al. 2020, Tables 2–3 | |
| free-form coupler to standard SMF-28 | 0.8 dB | Ranno et al., Photonics Res. 12, 1055 (2024) | |
| thin-film lithium niobate edge coupler | 0.24–0.29 dB, but to a 4.8 µm-MFD high-NA fibre, not SMF-28 | Chen et al., APL Photonics 9, 116111 (2024) | |
| free space | $\sim$0.97 dB at the theoretical best (80% Airy–Gaussian overlap); $\sim$1.6 dB measured on a diffraction-limited beam | Jovanovic et al., A&A 604, A122 (2017), after Shaklan & Roddier, Appl. Opt. 27, 2334 (1988) |
What CV-QKD labs charge themselves:
| System | Charged | Source |
|---|---|---|
| QOSST, 25.2 km spool | 0.47 dB to the connectors, and 0.23 dB per polarisation-maintaining mating sleeve | Quantum 8, 1575 (2024) |
| Hajomer et al., 100 km | 15.4 dB over 0.146 dB/km fibre, leaving 0.8 dB across two mode-field-diameter mismatch junctions | Sci. Adv. 10, eadi9474 (2024) |
q.Coupling has no default: published values span an order of magnitude and depend on what is coupled to what. Return loss is not modelled and there is no polish=: FC/PC and FC/APC cost about the same forward loss and differ in return loss, roughly 50 dB flat against 60 dB angled.
Loss sites
Every loss carries a site — "launch" at Alice's output, "receive" at Bob's input, "span" between them:
the same number however the decibels are distributed. A noise born inside the span — Raman, Rayleigh backscatter — crosses Bob's receive optics with the signal, so that loss cancels out of its shot-noise ratio:
| Move 1.5 dB of coupling | detector-plane rows | input-referred Raman | |
|---|---|---|---|
| at Bob's input | unchanged | unchanged | unchanged |
| at Alice's output | unchanged | unchanged | +1.5 dB — Alice's coupling attenuates the signal before the scattering, Bob's attenuates signal and scattered light together after it |
q.Link hands fibre-born impairments test_born_in_span). assemble(raman_db=…) is the only row of its own split the same way (test_raman_span puts raman and adc on opposite sides of the same
The four planes
A plane is a place on the optical path a noise is quoted at, named after hardware, never after a party. .at(plane) is the only way to move an entry.
| Plane | Where it is | Factor from the channel input |
|---|---|---|
"channel_input" | Alice's output, where | |
"channel_output" | the far end of the span, before any receive-side optics | |
"detector_input" | the detector's front face, after those optics, before | |
"post_detection" | after the detector's quantum efficiency |
"input" and "bob" alias channel_input and detector_input; anything else raises.
bud.at("channel_input") # {'phase': 0.010010, 'adc': 5.118e-06, ...}
bud.at("channel_output") # {'phase': 0.002744, 'adc': 1.403e-06, ...}
bud.at("detector_input") # {'phase': 0.001943, 'adc': 9.934e-07, ...}
bud.at("alice") # ValueError: plane must be one of ('channel_input', ...)The same noise at two planes
.refer(xi, start, end) moves one number — a
bud.refer(1e-3, "detector_input", "channel_input") # 0.005152
bud.refer(1e-3, "channel_output", "channel_input") # 0.003648The factor of 1.41 between the answers is the 1.5 dB of coupling between the two planes:
test_plane_compose). .at(plane) is refer() row by row from the channel input.
post_detection takes an , as an argument
bud.at("post_detection")
# ValueError: post_detection needs eta=: a budget carries no quantum efficiency
# and will not assume a perfect detector
bud.at("post_detection", eta=0.6) # every value multiplied by T * 0.6Budget field — no eta, no v_el (test_plane_detector). Past the detector is one factor of at().
On a run
res.budget is the same object, assembled from the run: what the channel did, beside res.est — what Alice and Bob measured from a finite sample — and res.oracle. None for the click families.
res = q.Link(
modulation=q.GaussianModulation(v_a=5.0),
channel=q.Fiber(length=25.0),
bob=q.Bob(detector=q.Heterodyne(eta=0.6, v_el=0.1)),
impairments=(q.Coexistence(channels=4, launch=0.0),),
losses=(q.Coupling(loss=1.5, site="receive"),),
).run()
res.budget.total # the xi the rate was computed at
res.budget.at("channel_input") # {'channel': 0.0, 'raman': 0.018082}
res.budget.at("detector_input")["raman"] # the same noise, one factor of T later
res.budget.losses # the itemised chain, fibre includedWhy is not a budget line
budget.assemble(v_a=5.0, t=10**-0.5, v_err=2e-3, vel=0.1)
# TypeError: assemble() got an unexpected keyword argument 'vel'q.Homodyne(v_el=…) or q.Heterodyne(v_el=…), never folded into cv_rate(..., trusted=False) and nowhere else.
| Layer | Owns | Pinned by |
|---|---|---|
| budget | test/consistency.py: no untrusted budget can be built to feed | |
| key rate | test/consistency.py: the untrusted rate equals a trusted rate on the hand-merged channel |
At
The measured envelope
What real systems report (defaults survey):
| System | Plane | Source | |
|---|---|---|---|
| Jouguet 2013, 80 km, TLO | 0.001–0.002 (0.007–0.008 worst case) | input | arXiv:1210.6216 |
| Zhang 2020, 27–203 km, TLO | 0.0015–0.0086 (0.0383 worst case) | input | arXiv:2001.02555 |
| QOSST benchmarks, 0–25 km, LLO | 0.0062–0.0095 | Bob | arXiv:2404.18637 |
| Hajomer 2024, 100 km LLO | 2.12e-4 | output | arXiv:2305.08156 |
| Hajomer 2024, 10 GBaud, DM | 0.0159–0.0718 | input | arXiv:2305.19642 |
| Laudenbach 2018, worked example | 0.0653 | output | arXiv:1703.09278 |
Zhang's values are transmitted-LO, low partly for want of two-laser phase noise; QOSST and Hajomer are the LLO comparators. Validation target: a default 25 km run lands at test_metro_envelope).
Reproducing Hajomer 2024
The closest published system to qkd's default configuration: CW light, locally generated LO, heterodyne, frequency-multiplexed pilot, 100 km of ultra-low-loss fibre.
h = budget.assemble(
v_a=8.41, # published V_mod
t=10**-1.54, # published 15.4 dB
v_err=7e-4, # FITTED, rad^2
rin=-155.0, # FITTED, dBc/Hz
bandwidth=100e6, # FITTED, Hz
dac_bits=16, # published
adc_bits=16, # published
)
h.total # 0.007385 input-referred
h.total * h.T # 2.130e-04 Bob planePublished channel-output q.Heterodyne(v_el=0.06272), not the budget. Three parameters are fitted — Tier B, with their allowed ranges and plausibility in Validation.
The
Further impairments
qkd.impairments holds the sources beyond [L18] Section 9 and the click-protocol observables that are not excess noise. Its assemble_extra() rows concatenate onto a Budget.
What the budget does not do yet
| Gap | State |
|---|---|
| CMRR ([L18] Eq. (9.80)) | not implemented |
| A measured Raman density on a run | assemble(raman_db=…) is budget-only: no q.Link consumes an q.Coexistence, [K21]'s launch-power parameterisation. Declaring both would count one mechanism twice |
Hardware rows on a q.Link | which rows a run charges and which it refuses: Status. The rest are assembled here and pinned onto q.Channel(xi=…, ref="input"); a pinned q.Channel refuses losses=, so fold the chain into T |
References
| [K24] | Kish et al., Quantum 8, 1382 (2024), arXiv:2206.13724 |
| [MA17] | Marie & Alléaume, Phys. Rev. A 95, 012316 (2017) |
| [S21] | Shen et al., Opt. Express 29, 30978 (2021), arXiv:2107.01798 |
| [U12] | Usenko et al., New J. Phys. 14, 093048 (2012) |
| [L18] | Laudenbach et al. 2018, arXiv:1703.09278 |
| [L10] | Leverrier et al. 2010 |
| [LP19] | Laudenbach & Pacher, arXiv:1904.01970 |
| [K21] | Kumar, Qin & Alléaume — see Impairments |