Skip to content

Noise budget ​

qkd.budget assembles ξ as an output of hardware parameters, each row tagged with the plane its source formula was stated in. SNU throughout (the SNU boundary); entries stored input-referred. Also reachable as q.budget.

python
from qkd import budget

Assembling a budget ​

python
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-03

1.090×10−2 SNU at the channel input, inside the measured envelope of 25 km systems; residual phase noise dominates by an order of magnitude. The phase row has two published forms.

A parameter left out omits its row rather than reporting a zero:

python
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-03

assemble(...) ​

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 T, and Entry.plane_note records it.

ParameterUnitPlaneDefaultDescription
v_aSNUinputrequiredVA>0 at the channel input.
t——requiredTransmittance of the fibre span alone, T∈(0,1]. Itemised optics multiply in: Budget.T is t times the chain, and that product divides every Bob-plane formula.
v_errrad²inputNoneResidual phase-error variance after pilot-assisted recovery (which one). Emits phase, a property of the recovered phase and so not divided by T.
xiSNUinput0.0Excess noise the channel already carried, renormalised alongside VA by phase and by nothing else. Not a row.
phase_form——"estimator""estimator" or "literature"; anything else raises.
rindBc/HzBobNoneRIN of signal laser and LO. Emits rin_sig and rin_lo.
bandwidthHz—NoneDetection bandwidth B, for rin and raman_db.
raman_dbdBm/nmBobNoneMeasured spontaneous-Raman spectral density NRam at the quantum channel. Emits raman. Referred by TlaunchTspan, not the total — loss sites.
wavelengthm—1550.12e-9λ, for raman_db alone: hf=hc/λ and the filter width λ2B/c. Default ITU channel 34, Laudenbach's 193.4 THz.
dac_bitsbitBobNoneAlice's DAC resolution b. Emits dac.
adc_bitsbitBobNoneBob's ADC resolution n. Emits adc.
losses——()q.Connector, q.Splice, q.Coupling — see the loss chain.
mu——2.0Detector multiplicity inside adc: 1 homodyne, 2 heterodyne, whose two ADCs' quantisation noise does not halve at the beamsplitter (Laudenbach Eq. (9.79)).
ratio—Bob10.0ADC full scale in shot-noise standard deviations, r=RU/σshot; default ±5σ.
InputResult
negative v_err or xiraises, not clamped. Both forms of ξphase are monotone through zero, so the literature form returns −6.49 SNU at Verr=−1, VA=5
xi with no v_errraises: 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 neitherValueError
vel= or trusted=TypeError — see why vel is not a budget line

Budget and Entry ​

Budget is a frozen dataclass, so its entries concatenate with impairments.assemble_extra()'s.

MemberTypeDescription
Budget.entriestuple[Entry, ...]One per described source, in emission order.
Budget.TfloatChannel input to detector input — the span and every itemised optic.
Budget.launchfloatTransmittance of the optics at Alice's output; 1.0 when none declared.
Budget.receivefloatTransmittance of the optics at Bob's input; 1.0 when none declared.
Budget.spanfloatT/(launch⋅receive).
Budget.lossestuple[Loss, ...]The chain in optical order, the fibre's own attenuation among it as fiber_span.
Budget.totalfloatSum of every entry's ξ, input-referred, SNU.
Budget.factor(plane, eta=None)floatFactor 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)floatOne number moved between two planes — for a ξ this budget did not produce.
Budget.table()strThe aligned text table above.
Entry.sourcestrA row name from the per-source table.
Entry.xifloatInput-referred, SNU.
Entry.plane_notestrThe citation and the plane its source paper stated it in.
Loss.sourcestrfiber_span, connector_launch, splice_span, coupling_receive.
Loss.dbfloatThe row's insertion loss in dB, count included.
Loss.sitestr"launch", "span" or "receive".
Loss.transmittancefloat10−db/10.

The per-source table ​

Bob-plane rows are divided by T before storing.

SourceFormulaStated atReference
phaseξ=(VA+ξch)(eVerr−1), or 2VA(1−e−Verr/2) under 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ξ=TVmod(d+d2/2)2, d=π/(2b12)Bob[L18] Eq. (9.37)
adcξ=μr2/(12⋅22n)Bob[L18] Eq. (9.108), quantisation term
rin_sigξ=TVmodRINBBob[L18] Eq. (9.9)
rin_loξ=14RINBV(q^)Bob[L18] Eq. (9.21)
ramanξ=2Δλ10NRam/10τ/(hf)×106Bob[L18] Eq. (9.63), over Eq. (9.59) ξ=2⟨n⟩ and Eq. (9.60) ⟨n⟩=Pτ/(hf)

No detector row and no trusted flag — why.

Two connecting substitutions are qkd's, not verbatim in the sources:

RowSubstitution
dacδU=LSB/12 for pure quantisation of an ideal b-bit DAC, the full DAC range driving one half-wave voltage (gRU=Uπ)
adcEq. (9.108)'s opto-electronic prefactor reduced to one ratio r=RU/σshot (default 10, i.e. ±5σ); the components carry no g, ρ, τ or PLO

The raman row ​

NRam is measured: what an OSA reads at the quantum wavelength with the classical traffic running. [L18]'s Δλ is the filter the light is read through; raman_width() supplies λ2B/c, since in a coherent receiver only modes within B of the LO beat into the quadrature. raman_width(1e9)=8.015 pm reproduces his Δλ≈8 pm at B=1 GHz on the 1550 nm / 193.4 THz channel.

A matched receiver, τ=1/B through width λ2B/c, cancels B:

ξRam=2λ3pRamhc2,pRam=10NRam/10×106 W/m.

assemble still takes bandwidth: B enters raman() as two separate parameters, and either alone moves the answer.

Two conventions, a factor of two apart. [L18] carries no 12 for polarisation: his footnote to Eq. (9.63) cancels the polarising beamsplitter against the LO's doubled mixing bandwidth. [K21], behind 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 2.

The formulas individually ​

Each returns an input-referred ξ in SNU.

FunctionArgumentsReturns
phase(v_a, v_err, xi=0.0, *, form="estimator")VA and ξch in SNU, Verr in rad²either form
dac(v_a, bits, t)VA in SNU, b in bits, TVA(d+d2/2)2 with d=π/(2b12)
adc(bits, t, mu=2.0, ratio=10.0)n in bits, T, multiplicity, rμr2/(12⋅22nT)
rin_sig(v_a, rin_db, bandwidth, t)VA in SNU, RIN in dBc/Hz, B in Hz, TVARINB
rin_lo(rin_db, bandwidth, quad_var, t)RIN in dBc/Hz, B in Hz, V(q^) at Bob in SNU, TRINBV(q^)/(4T)
raman_width(bandwidth, wavelength=1550.12e-9)B in Hz, λ in mΔλ=λ2B/c in m
raman(n_ram, width, symbol, t, *, wavelength=1550.12e-9)NRam in dBm/nm, Δλ in m, τ in s, TlaunchTspan2Δλ10N/10τλ/(hcT)×106
python
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 = 1

assemble supplies rin_lo's V(q^) as TVA+1, dropping ξrest and the electronic term as second order.

The two forms of the phase term ​

ξphaseestimator=(VA+ξ)(eVerr−1),ξphaseliterature=2VA(1−e−Verr/2)

ξ is the channel's input-referred excess noise. Both forms are stated at the channel input; neither is divided by T.

Which Verr ​

Both read Verr through one channel, the surviving correlation κ=e−Verr/2:

Verr=−2ln⁡E[cos⁡(θ−θ^)],

[K24] Eq. (25)'s r¯=⟨eiθ⟩=e−σ2/2. It is not the second moment of the residual. src/pipeline.rs reports both:

FieldQuantityCeilingFeeds
SimOut.v_err−2ln⁡E[cos⁡φ]nonethe budget
SimOut.v_wrapthe wrapped second moment about zeroπ2/3=3.29, the wrap interval's own variance, while the correlation keeps falling to zeroa phase tracker's own error budget

Only θmod2π reaches the frames, so any moment of the residual is of a wrapped angle ([K24], before Eq. (24)).

Where the estimator form comes from ​

[K24] is the citation of record: it concerns a phase variance and names the model it corrects.

SourceRoute
[K24] App. E, Eqs. (76)–(82)the channel Alice and Bob infer through a phase-noise channel. The correlation carries r¯=e−σ2/2, so the inferred transmittance is ηI=ηr¯2=ηe−σθ2, and the inferred noise χI is renormalised by that factor, its signal part μ(eσθ2−1) with μ the variance at the referred plane
[S21] Eqs. (18)–(19)a deterministic receiver angle: ε′=ε/cos2⁡θ+tan2⁡θ⟨xA2⟩=ξ+(VA+ξ)(1/cos2⁡θ−1), the same once cos2⁡θ is identified with κ2
[U12]the fading-channel form, independently; the passage is unnumbered

(VA+ξ)(eVerr−1) never appears literally in [K24]: it follows from Eqs. (80)–(82) by rearrangement, and is cited that way. budget.phase returns the increment over ξ.

infer(), the transmittance half ​

[K24] Eq. (78) carries the other half of the rotation, ηI=ηr¯2:

python
budget.infer(t, v_err, form="estimator")   # -> t * exp(-v_err)

in whatever plane t is stated. The halves pin:

infer(t,v)(VA+ξ+phase(VA,v,ξ))≡t(VA+ξ),

so charging the noise half against an unattenuated t overstates the rate. No domain; v_err = inf returns 0.0. form="literature" is refused:

python
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 stands
Budget.Tthe physical loss chain, unmoved
Budget.inferredthe same T as parameter estimation infers it, Te−verr. Equal to 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 ξ moves for it, and the product goes on 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 2VA(1−κ): the noise an observer who already knows T and ξ attributes to the lost correlation. Alice and Bob fit the channel; the random phase shrinks the fitted slope to t^=t0κ, and parameter estimation divides the residual variance by t^2 ([L10] Eq. (20), src/pipeline.rs). Through the regression,

σ^2=t02VA(1−κ2)+t02ξ+1+vel,ξ^−ξideal=σ^2−1−velt^2−ξ=(VA+ξ)(1κ2−1).

The renormalisation is the difference, and why ξ appears in one form and not the other. [K24] make this criticism of [MA17] by name at the end of their Appendix E.

Which plane, and which denominator ​

The second-order answer depends on plane and denominator — the referral trap at second order.

ChoiceWhat is shipped
PlaneThe channel input (Alice), for both VA and ξ, in SNU with the vacuum at 1 per quadrature. Every budget entry lives there.
SlopeThe least-squares t^=∑ay/∑a2 of Bob's quadrature on Alice's symbol, and the residual variance about that fitted line.
Denominatort^2, the fitted slope, not the design slope t02.
ReceiverCalibrated inside the estimator: vel subtracted before the division and η carried inside the slope, i.e. ξ^=(σ^2−1−vel)/t^2 and T^=2t^2/η, exactly as src/pipeline.rs computes them.

The last row belongs to the estimator, not the security model: a rate from these estimates may still hand η and vel to Eve through trusted=False. Referring to Bob's plane, or dividing by t02, changes the second-order term.

They agree to first order, then separate ​

With ξ=0 both reduce to VAVerr as Verr→0, and the relative gap opens linearly:

ξestimator−ξliteratureξestimator=34Verr+O(Verr2),ξestimatorξliterature=eVerr+eVerr/22.

The ratio is analytic: with this Verr both forms are identities, not approximations. With u=e−Verr/2 it is (1+u)/(2u2)>1 for every u<1, so the literature form is strictly the smaller at every Verr>0.

Verr (rad²)estimatorliteratureliterature is low by
10−30.0050030.0049990.07%
2×10−30.0100100.0099950.15%
10−20.0502510.0498750.75%
0.050.2563550.2469013.7%
0.10.5258550.4877067.3%
0.281.6156491.30641819.1%
1.08.5914093.93469354.2%

VA=5 SNU, ξ=0, input-referred.

Measured against the simulator's own estimator ​

ξ^−ξideal — recovered frames against the same frames derotated by the oracle phase — is the excess noise phase recovery cost. Linewidth sweep at VA=5, T=0.5, ξ=0.01, η=0.6, vel=0.1, 106 symbols, seed 7; positive means the expression sits below the measurement.

Verrmeasuredliterature is low byestimator is off by
0.00240.0126+3.4%+3.1%
0.05350.2771+5.0%+0.8%
0.15750.8557+13.0%+0.3%
0.27151.5745+24.0%+0.9%
0.39902.4676+36.5%+0.6%
0.59574.0817+58.5%+0.2%
1.151310.9099+149.3%+0.8%
2.068335.0527+443.9%+1.4%
ColumnBehaviour
literaturemonotone, one-signed, following the analytic ratio above
estimatorflat: an identity in Verr, the residual being the sampling error of VA+ξ at 106 symbols. Five seeds scatter each entry by about 2 points; the first row, at 0.0126 SNU, is noisiest. At Verr=4.15, excess noise 306 SNU, it lands within 2.3%
the run's own slope ratio t^/tideal substituted for κreproduces the measurement to better than 0.06% at every point, the residual constant across linewidths for a given seed — test_estimator_slope_ratio

A modelling error, not a security hole: a deployed system measures ξ, where the estimator bias makes the measured value the larger and the claimed rate lower. The literature form biases the predicted number, ξ under and rate over.

The literature form's domain ​

FormDomainPast it
"literature"PHASE_LIMIT = 0.1 rad²saturates at 2VA — 10 SNU at VA=5 — while measured excess noise has no ceiling: 6.44 SNU at Verr=2.07 against a measured 35.05, the gap past a factor of 19 by 3.48. Emits a PhaseDomainWarning naming Verr and the limit, and still returns the number
"estimator"noneeVerr−1 is 1/κ2−1 for a phase distribution of any width and shape. Never warns

The 0.1 is [K24]'s figure but not a domain they put on Eq. (85): their sentence bounds the further linearisation ξx=ξp≈σ~Θ22VA for σ~2<0.1, and Eq. (85) is printed with no stated range. qkd uses it as the range over which the literature form tracks the estimator one — a house reading of that sentence, not a quotation.

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 ​

python
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")
Taskform=Why
Predicting a rate, or comparing against anything this simulator measures"estimator", the defaultit is what parameter estimation reports
Reproducing a published number computed with the other form"literature"[MA17] Eqs. (10)–(11) state 2VA(1−e−Verr/2), reproduced as [K24] App. F, Eq. (85). Every anchor fitted against that form selects it explicitly and says so in its report row

Entry.plane_note names the expression behind each phase row.

Link with a DSP chain derives Verr and applies the estimator form, the channel's ξ as second argument. There is no 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 ​

T=∏i10−ℓi/10.

python
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 linkeach decibel has a named row
adc moves to 5.118×10−6 from 3.141×10−6stated at Bob's plane and referred to the input by the total T: 2.12 dB more link is 2.12 dB more input-referred quantisation noise
phase, dac, rin_sig do not moveAlice-plane quantities
a second descriptor of the same kind at the same siteraises rather than merging. count= multiplies the decibels on one line

What each one is, and what it costs ​

ComponentWhat it is physicallyTypicalSource
q.ConnectorA 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 ≤0.25 dB, max ≤0.50 dB, measured per IEC 61300-3-34. Thorlabs quotes 0.25 dB typical FC/APC, 0.3 dB FC/PC
grades B / C / D, mean (max)0.12 (0.25) / 0.25 (0.50) / 0.50 (1.00) dBIEC 61753-1 attenuation grades, the max holding for $>$97% of samples
q.SpliceA fusion splice: core eccentricity and mode-field mismatch.0.02 dB core-aligning, 0.03–0.04 dB v-grooveCorning/AFL AN0041, SMF-28 Ultra at 1550 nm, measured per ANSI/TIA/EIA-455-8. Telcordia GR-20-CORE asks for a group mean ≤0.10 dB
q.CouplingLight 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 defaultthe five rows below
silicon grating coupler3.1 dB, or 1.0–1.6 dB with a bottom reflectorMu et al., Appl. Sci. 10, 1538 (2020), Table 2; Cheng et al., Micromachines 11, 666 (2020)
silicon inverse-taper edge coupler1.3–1.5 dBMu et al. 2020, Tables 2–3
free-form coupler to standard SMF-280.8 dBRanno et al., Photonics Res. 12, 1055 (2024)
thin-film lithium niobate edge coupler0.24–0.29 dB, but to a 4.8 µm-MFD high-NA fibre, not SMF-28Chen et al., APL Photonics 9, 116111 (2024)
free space → single-mode fibre$\sim$0.97 dB at the theoretical best (80% Airy–Gaussian overlap); $\sim$1.6 dB measured on a diffraction-limited beamJovanovic et al., A&A 604, A122 (2017), after Shaklan & Roddier, Appl. Opt. 27, 2334 (1988)

What CV-QKD labs charge themselves:

SystemChargedSource
QOSST, 25.2 km spool0.47 dB to the connectors, and 0.23 dB per polarisation-maintaining mating sleeveQuantum 8, 1575 (2024)
Hajomer et al., 100 km15.4 dB over 0.146 dB/km fibre, leaving 0.8 dB across two mode-field-diameter mismatch junctionsSci. 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:

T=TlaunchTspanTreceive,

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:

ξinput=ξspanTlaunchTspanand notξspanT.
Move 1.5 dB of couplingTdetector-plane rowsinput-referred Raman
at Bob's inputunchangedunchangedunchanged
at Alice's outputunchangedunchanged+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 TlaunchTspan and everything else the total, so launch-side placement costs key at identical T (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 1.5 dB of receive coupling).

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.

PlaneWhere it isFactor from the channel input
"channel_input"Alice's output, where VA is defined and measured1
"channel_output"the far end of the span, before any receive-side opticsTlaunchTspan
"detector_input"the detector's front face, after those optics, before ηT
"post_detection"after the detector's quantum efficiencyTη

"input" and "bob" alias channel_input and detector_input; anything else raises.

python
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 ξ measured at the fibre end, or quoted at Bob:

python
bud.refer(1e-3, "detector_input", "channel_input")   # 0.005152
bud.refer(1e-3, "channel_output", "channel_input")   # 0.003648

The factor of 1.41 between the answers is the 1.5 dB of coupling between the two planes:

ξend=ξstartf(end)f(start),

f the factor column. Composition and A→B→A as the identity hold over every ordered pair (test_plane_compose). .at(plane) is refer() row by row from the channel input.

post_detection takes an η, as an argument ​

python
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.6

η belongs to the receiver: a query parameter, never a Budget field — no eta, no v_el (test_plane_detector). Past the detector is one factor of η; electronic noise and the heterodyne vacuum unit are the key-rate layer's arithmetic, unreachable from 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.

python
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 included

Why vel is not a budget line ​

python
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'

vel reaches the key-rate layer as q.Homodyne(v_el=…) or q.Heterodyne(v_el=…), never folded into ξ. The untrusted case is the substitutionξ→ξ+μvel/(ηT) ([LP19] Sec. 2, Eqs. (13)/(14)), applied by cv_rate(..., trusted=False) and nowhere else.

LayerOwnsPinned by
budgetξ — what the channel and the transmitter did to the light. No η, so it could not form the term abovetest/consistency.py: no untrusted budget can be built to feed vel twice
key rate(η,vel,trusted) — what the receiver and the threat model do to ittest/consistency.py: the untrusted rate equals a trusted rate on the hand-merged channel

At T=10−0.5, η=0.6, vel=0.1 heterodyne, that term is ≈1.05 SNU against a 10−2 SNU channel budget.

The measured envelope ​

What real systems report (defaults survey):

SystemξPlaneSource
Jouguet 2013, 80 km, TLO0.001–0.002 (0.007–0.008 worst case)inputarXiv:1210.6216
Zhang 2020, 27–203 km, TLO0.0015–0.0086 (0.0383 worst case)inputarXiv:2001.02555
QOSST benchmarks, 0–25 km, LLO0.0062–0.0095BobarXiv:2404.18637
Hajomer 2024, 100 km LLO2.12e-4outputarXiv:2305.08156
Hajomer 2024, 10 GBaud, DM0.0159–0.0718inputarXiv:2305.19642
Laudenbach 2018, worked example0.0653outputarXiv: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 ξ≈0.005–0.03 SNU at the channel input (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.

python
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 plane

Published channel-output ξ=2.12×10−4 SNU; the budget lands 0.47% high. Their vel=0.06272 SNU reaches a rate as q.Heterodyne(v_el=0.06272), not the budget. Three parameters are fitted — Tier B, with their allowed ranges and plausibility in Validation.

The verr fit was made against the literature form and stands under the estimator default: at 7×10−4 rad² the forms differ by 0.05%, moving the Bob-plane total from 2.1290×10−4 to 2.1299×10−4.

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 ​

GapState
CMRR ([L18] Eq. (9.80))not implemented
A measured Raman density on a runassemble(raman_db=…) is budget-only: no q.Link consumes an NRam. A run reaches Raman through q.Coexistence, [K21]'s launch-power parameterisation. Declaring both would count one mechanism twice
Hardware rows on a q.Linkwhich 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