Skip to content

Impairments ​

Noise sources outside Laudenbach's Section 9, and click-protocol observables that are not excess noise. Companion to qkd.budget.

python
from qkd import impairments as im

An impairment is imperfect hardware; an attack is Eve exploiting it. Each model here returns a budget.Entry a rate is computed from, or an observable reported beside one; qkd.attacks returns a Reading, which is neither. The same τd is a rate cost as q.DeadTime and a blanking attack's hiding place.

Descriptors are frozen dataclasses. ξ contributors go through assemble_extra(), observables through security(), and catalogue() lists both.

Every model names a plane ​

Rows are SNU and stored at channel_input, as budget stores its own, so res.budget.at(plane) moves both by one factor (four planes). Stated at is the plane the source wrote its formula in; Referred by is the operation that moved it, recorded in Entry.plane_note.

ModelFormulaStated atReferred bySource
ramanξ=2⟨N⟩/(ηDT)Bob/TKumar 2015, Eqs. (6)–(7)
rayleighξ=2⟨n⟩/T, ⟨n⟩=12fbsPτ/(hf)Bob/TMandil 2024, Eq. (A1), into Laudenbach Eqs. (9.59)–(9.60)
dephasingbudget.phase at Vϕ=2πΔνtinputnot dividedQi 2015, Eqs. (9)–(10)
polarisationξ=Var(η)VA/⟨η⟩2, η=cos2⁡ΦBob/TeffUsenko 2012, unnumbered; overlap from Sharma 2024
imbalanceξ=12VA|deiθm−1|2BobTηe strippedWang 2025, Eq. (E1)
timingξ=Var(η)VA/⟨η⟩2, η=e−δ2/(2w2)Bob/TeffUsenko 2012, unnumbered, on a Gaussian matched-filter overlap

Teff=⟨η⟩2 is not T. polarisation and timing are fading channels: Usenko splits each into a fixed channel of transmittance ⟨η⟩2 plus a Bob-plane excess noise. That noise is referred to the input by Teff; the fibre's T cancels. Both scale as the fourth power of their small parameter, since ⟨η⟩ and ⟨η⟩2 agree to second order.

A fading row is two halves ​

HalfWhere it isReaches
the noise half, Var(η)VA/⟨η⟩2polarisation(), timing(), and the polarisation/timing rows of assemble_extra()the budget, as a budget.Entry
the transmittance half, ⟨η⟩2pol_fading()[0], jitter_fading()[0], composed by fading_factor()the plane the rate is claimed at — explain["T_claimed"], never explain["T"]

fading_factor(pol=…, clock=…) multiplies the halves present and returns 1.0 when neither is given. No other descriptor has a transmittance half, and it takes no other keyword.

The two halves are one channel:

Tf(VA+ξfade)=TVA⟨η⟩,

so charging the noise half against an unattenuated T overstates the rate. q.Link composes both into explain["T_claimed"].

dephasing is input-referred because its source states it there. dephasing(v_a, linewidth, delay, xi=0.0, form=…) and Dephasing(linewidth, delay, form=…) take budget.phase's selector and its "estimator" default.

Observables carry no plane:

ObservableReturnsObserved atSource
backflash_leakPlearn=PbPsift, the fraction of the sifted key Eve readsEve's tap on the fibre outside BobSingh 2025
backflash_ratePsec=Psift(1−Pb−fh(e)), clamped at zerothe sifted keySingh 2025
extinctione′=(1−4r+3)e+2r+3, a QBERBob's sifted bitsHuang 2012, Eqs. (1) and (5)
visibilityV=cos⁡Φ, a fringe contrastthe receiver's interferenceSharma 2024
dgd⟨Δτ⟩=DPMDL, in secondsthe fibre spanAntonelli 2024
saturatedetected click rate after dead-time lossesBob's detectorKrause 2025, Eq. (1); Rogers 2007
afterpulse(Qμ,Eμ), a gain and a QBERBob's detectorPapapanos 2020, Eqs. (4), (7) and (8)

The default f is not the source's ​

backflash_rate charges f=1.16: Lütkenhaus 2000, Table I, Brassard–Salvail bidirectional reconciliation at e=0.01 and 0.05, and q.IndividualAttack's default. Singh 2025 write "the usual value for f is 1.15" and evaluate there. h cancels, so the gap is a constant 0.862 % of the error-correction term at every QBER; on the anchor it moves the published "approximately 10 %" reduction to 10.024 % from 10.036 %. f=1.15, directly or through security(..., f=1.15), reproduces the paper.

Backflash is zero-disturbance: no excess noise, no observable moved. It is reported as LinkResult.leak and never folded into a rate.

Sourced, derived and measured ​

StatusMeans
publishedthe closed form and its constants come from the cited source
qkd connectsthe source states an ingredient; an unpublished elementary step joins it to the result. Flagged in the docstring of the function that performs it, in the wording budget.dac uses for its own substitution
measured inputthe parameter is a measurement of one device, with no model predicting it from anything more primitive
diagnosticcomputed and reported, never converted into a ξ, because no verified conversion exists

What each model owes its source ​

Published is what the primary source states, to the equation. qkd's own step is everything joining it to the returned number, including constants fitted to close an anchor.

MechanismStatusPublishedqkd's own step
Raman (raman)published, on a measured inputKumar Eqs. (6) and (7): the SASRS photon number for both launch geometries, and its conversion to a channel-input ξ. Behind it, Laudenbach Eq. (9.59) ξ=2⟨n⟩ with ⟨n^⟩=12V, and Eq. (9.60) converting optical power to photon number.The parameterisation: Kumar Eq. (6) takes launch power, fibre and geometry. Laudenbach Eq. (9.63) takes a measured density and ships as budget.raman, a factor of two apart and consumed by no q.Link. Anchored on the paper's figures, 1.274×10−3 forward and 1.574×10−3 backward against their ∼1.3 and ∼1.6×10−3N0. Those quoted numbers are 2⟨N⟩ at Bob, not the ξin of their Eq. (7), which pins the convention. β is a spectrum, so Coexistence.beta takes the measurement (gap 1).
Rayleigh (rayleigh)qkd connectsMandil Eq. (A1), the fibre's impulse response Ps(t)=P0τηe−αvgt; the OTDR-measured η=8.0±0.1 s−1 for SMF-28 and 6.54±0.08 for SMF-28 ULL; and the random scattered polarisation, which supplies the factor 12.The round-trip integral of that response for a continuous-wave launch, fbs(L)=η(1−e−2αL)/(αvg), saturating at η/(αvg). Flagged in rayleigh_fraction's docstring. Single backscatter only (gap 7). Subacius 2005, the canonical QKD statement, is paywalled and was not mined; nothing here rests on it.
Dephasing (dephasing)publishedQi Eqs. (9)–(10): ⟨(Δθ)2⟩=2t/τc with τc≃1/(πΔν), hence Vϕ=2πΔνt, and Δν=ΔνA+ΔνB for two independent lasers. The map to ξ is either published phase form.Composition of two published results. The default estimator form is prior art, not a house derivation.
Polarisation (polarisation, visibility, dgd)published; dgd diagnosticUsenko's fading identity: a channel of fluctuating transmittance is a fixed channel of transmittance ⟨η⟩2 plus a Bob-plane Var(η)(V−1). The mode overlap is Sharma's, the mismatch entering the relay output mean photon number through cos⁡δcos⁡Φ. ⟨Δτ⟩=DPMDL is standard; Antonelli applies it to a quantum channel.The Gaussian average of the overlap over a drifting angle: ⟨η⟩=⟨cos⁡Φ⟩=e−σ2/2 and ⟨η⟩=12(1+e−2σ2), so Var(η)=12(1−e−σ2)2 and ξpol→VAσ4/2 as σ→0. Sharma's Φ≤11∘ tolerance stands. Sharma's V<0.37 is not qkd's to use: it is a polarisation-MDI threshold imported from Yuan 2014 and Xu 2013 and drawn as a dashed reference line; nothing in qkd rests on it. The PMD branch stops at the DGD (gap 2).
Modulator (imbalance, extinction)publishedWang Eq. (E1), Var[εq1]B=TηeVA2[d2sin2⁡θm+(dcos⁡θm−1)2], with their measured floor d≥0.9937 and noise under 0.02 SNU for |θm|≤5∘. Huang Eqs. (1) and (5) for the extinction QBER. The 4/(r+3) admixture is identity, invariant under every unitary, so Eve learns nothing from it and privacy amplification shrinks only the remaining 1−4/(r+3).Stripping Tηe to reach the input plane, and reading the bracket as |deiθm−1|2, the squared miss of the complex modulation gain. Amplitude imbalance and quadrature bias drift are therefore one term. The anchor fits VA=5.25 SNU, which the source does not tabulate. extinction holds for a four-modulator click transmitter and returns a QBER; it reaches security(), never assemble_extra() (gaps 3 and 4).
Timing (timing)qkd connectsUsenko's fading identity, unchanged.The matched-filter overlap ρ(δ)=e−δ2/(4w2) of a Gaussian envelope sampled at offset δ, averaged over a jittering instant with ⟨e−aδ2⟩=(1+2aσj2)−1/2. Both flagged in jitter_fading's docstring. Only u=(σj/w)2 survives, so tolerable absolute jitter scales with the symbol period.
Backflash (backflash_leak, backflash_rate)published, on a measured inputSingh's Pb=nEve/nsift, Plearn=PbPsift and Psec=Psift(1−Pb−fh(e)), with 1598 backflash events against 18000 sifted counts on a 74 % SNSPD, arriving within <5 ns of the click.Pb belongs to one detector at one excess bias and gate width (gap 5), so Backflash(prob) takes the measurement. The anchor QBER is fitted at e=1.29%, a COW operating point the source does not state; with it and f=1.16 the module reproduces their 10.0 % key-rate reduction, and the test message names the fit. Meda's PL=9.8% and 6% are a plausibility range, never a default (gap 6). Li's ceiling — Eve extracts usable information from at most 95.7 % of backflash photons, the broadband spectrum degrading the extinction she can measure — is recorded, not implemented.
Dead time (saturate, afterpulse)publishedNon-paralysable R=R∗/(1+R∗τd), Krause Eq. (1), and paralysable R=R∗e−R∗τd, both standard. Rogers settle which applies to QKD: one SPAD is non-paralysable, a pair serving one basis is paralysable. Afterpulsing is Papapanos Eqs. (4), (7) and (8) over the Ma–Lo decoy model.At pAP=0 the expressions collapse onto the plain Ma–Lo gain and error rate, which the tests check. q.ClickDetector(dead_time=…, afterpulse=…) reaches the simulated click train, but no exam reproduces Rogers' conclusion, the transmission rate above which the sifted rate falls. Built, not anchored.

timing is the CV sampling branch. Click-path detector jitter is a different model, split into window loss and bin leak — Protocols, fitted in Validation. A pure Gaussian fitted to Diamanti et al.'s 100 ps window statement (192.13 ps FWHM) puts the neighbouring slot 12.3 standard deviations away, leak 5.4×10−31.

The gaps ​

Each open gap is open because the published constant or closed form does not exist.

#GapStatus, and why
1Spacing dependence of the Raman coefficient βOpen. β is the spacing dependence, a measured spectrum: Kumar report 1.5 to 3.1×10−9 km−1nm−1 across the C band with no fitted form, and silica's Raman gain has a broad 13 THz peak plus structure no two-parameter model captures. No published β(Δλ) accurate enough to ship was found; unverified.
2PMD to excess noiseOpen. Converting a DGD into ξ needs a depolarisation model for a pulse spanning both principal states; every treatment found is numerical or specific to entangled-pair spectra. To first order PMD on a narrowband CV signal is a unitary rotation, not depolarisation, so the drift model carries the load — here and in the reference-frame tracking model.
3Extinction ratio to a CV excess noiseOpen, narrowed. A residual carrier of known amplitude is a deterministic displacement, not noise unless it drifts, so the CV parameter is a drifting displacement variance, a different measurement from an extinction ratio. On the click side COW carries finite modulator extinction as q.IntensityKeying(extinction=…), and symbol-level BB84 derives its QBER from a simulated pulse train.
4Bias drift beyond the quadrature bias pointOpen; reduces to gap 3. Wang's θm covers the orthogonality bias point. A DC null-bias drift of the child modulators appears as carrier leakage, gap 3's quantity.
5Pb from hardwareOpen. Every source treats Pb as measured for one detector at one excess bias and gate width. Meda give the spectral and temporal shape — broadband across 1530–1600 nm, with a temporal signature that fingerprints the detector model — but no model from bias voltage or avalanche charge. Whether the avalanche-charge scaling implied by SPAD physics is published as a usable formula is unverified.
6The normalisation of Meda's PLOpen. Their 9.8 % and 6 % assume ηchηdet=0.05 on Eve's side. Which corrections sit in PL rather than the raw emission probability could not be reconstructed from the post-print.
7Double backscattering on a one-way linkOpen. On a one-way link single backscatter travels away from Bob; only the second-order forward term reaches him. It scales as fbs2≈10−6. No closed form was found; the caveat is in the module docstring.
8The two connecting integralsNot a gap: a disclosure. The continuous-wave integration of Eq. (A1) and the Gaussian matched-filter overlap are qkd's, flagged in code with the wording budget.dac uses. The overlap covers the CV sampling branch only; the click path's tailed arrival-time law and its window-loss/bin-leak split are also qkd's and also flagged.
9Composition of budget.phase and impairments.dephasingClosed. One mechanism at two idealisations: a run with both a DSP chain and a Dephasing charged the laser twice. See the double-count guard.
House rule
No verified conversionreport the ingredient and stop — the diagnostic status: dgd returns a differential group delay in seconds and no ξ
Constant exists only as a measurementthe descriptor takes the measurement (β, Pb)
A number fitted to close an anchorthe test message that uses it names the fit

Descriptors ​

Frozen dataclasses of hardware numbers, not validated at construction, unlike the q. components. im.Backflash(prob=2.0) constructs, and raises only when a leak is computed from it. Plane is the plane the source formula was stated in (planes).

Fibre ​

ParameterUnitPlaneDefaultDescription
Coexistence.channels—BobrequiredCo-propagating classical channels. Launch powers add linearly in the Raman source term, so ξ is linear in the count.
Coexistence.launchdBmBobrequiredPer-channel launch power. Kumar found a 25 km CV-QKD link tolerates up to 11.5 dBm forward and 9.7 dBm backward.
Coexistence.beta1/(km nm)Bob3.0e-9Raman coefficient, measured: Kumar report 1.5 to 3.1×10−9 across the C band for a 1531.12 nm quantum channel. The default is their worst case.
Coexistence.wavelengthmBob1531.12e-9Quantum-channel wavelength λ, entering the source term as λ3. The default is Kumar's measurement channel and pairs with the default β.
Coexistence.demux—Bob1.0Kumar Eq. (6)'s ηD, "transmittance of DEMUX (Add-Drop Module) place at Bob side" — not a detector efficiency, which is that paper's ηB at Eq. (5). It cancels through Eq. (7) and moves no input-referred ξ. A filter narrower than the quantum channel would not cancel; no rate models one, so q.Link refuses a declared value by name. Carried so raman_photons() reproduces the source's Bob-plane figures.
Coexistence.backward——FalseCounter-propagating classical channels: backward geometry (1−e−2αL)/(2α) in place of forward Le−αL.
Backscatter.powerWBobrequiredPower of the counter-propagating tone scattering back into Bob: a two-way architecture's outbound pulse train or a classical channel.
Backscatter.coeff1/sBob8.0Returned power at t=0 over forward pulse energy, measured by OTDR: 8.0±0.1 for SMF-28, 6.54±0.08 for SMF-28 ULL.
Backscatter.index—Bob1.468Fibre group index n; vg=c/n.
Backscatter.wavelengthmBob1550.12e-9The launch wavelength. Rayleigh scattering is elastic, so the return is in band and no spectral filter applies.
Polarisation.driftradBobrequiredRMS mismatch angle σ between signal and receiver reference. A static mismatch is pure loss; the drift produces excess noise.
Polarisation.dispersions/km—0.0PMD coefficient DPMD, reaching dgd() alone. q.Link refuses a declared value by name: no verified closed form converts a DGD into an excess noise.

Transmitter ​

ParameterUnitPlaneDefaultDescription
Dephasing.linewidthHzinputrequiredThe sum of both lasers' Lorentzian full widths; the beat phase diffuses at the sum rate.
Dephasing.delaysinputrequiredTime between the phase reference and the symbol it corrects. What diffuses over it is uncorrectable.
Dephasing.form—input"estimator"budget.phase's form=. Also selects the row's plane_note, naming Kish or Marie & Alléaume.
Modulator.ratio—Bob1.0I/Q amplitude imbalance d=b/a; 1.0 balanced.
Modulator.angleradBob0.0Quadrature bias-point offset θm, 0.0 orthogonal, and the axis that bias point drifts along. In |deiθm−1|2 amplitude imbalance and quadrature bias drift are one term. A DC null-bias drift of the child modulators is not covered.
Modulator.extinction——NoneLinear extinction ratio r=Ion/Ioff, a QBER. A click q.Link charges it and security() reports it; a quadrature link refuses a declared value by name, and assemble_extra() never reads it (gap 3). None omits the observable. Practical intensity modulators reach 20 to 40 dB; 20 dB alone costs 1.9 % QBER.
Timing.jittersBobrequiredRMS sampling-instant error: clock jitter plus residual drift.
Timing.widthsBobrequiredField envelope parameter w of e−t2/(2w2). Only u=(jitter/w)2 reaches the answer.

Detector ​

ParameterUnitPlaneDefaultDescription
Backflash.prob——requiredPb=nEve/nsift, the chance a sifted detection at Bob comes with a backflash photon reaching Eve. A leakage probability with no plane, checked against [0,1] by its consumers.
DeadTime.deads—requiredInterval τd after an avalanche during which the detector is held below breakdown. Tens of nanoseconds for a gated InGaAs device, of order 100 ns for a free-running SPAD.
DeadTime.afterpulse——0.0Probability pAP of a spurious count given a previous detection. Uncorrelated with its trigger, so it errs half the time, like a dark count. Of order 1 to 5 % for InGaAs/InP SPADs, essentially zero for superconducting nanowires.
DeadTime.paralysable——FalseWhether an arrival inside the dead time extends it. q.Link's decoy path refuses a declared value by name, taking a q.DeadTime only at dead = 0. One SPAD is non-paralysable, the default; a pair serving one basis is paralysable, since a click on either disables that basis's sifting. The non-paralysable rate saturates at 1/τd; the paralysable one peaks at R∗=1/τd and then falls.
python
im.saturate(1e6, 1e-7)                    # 909090.9  non-paralysable
im.saturate(1e6, 1e-7, paralysable=True)  # 904837.4  same arrivals, fewer counts
im.afterpulse(0.5, 0.2, 1e-6, 0.02)       # (0.0970669, 0.0098091)  gain, QBER

At R∗τd=0.1 the branches agree to half a percent; above it they diverge without limit.

assemble_extra() ​

Keyword-only. As in budget.assemble, a descriptor left None omits its row; one present but perfect, such as Modulator() at ratio 1 and angle 0, contributes 0.0. Keywords name the descriptor's role, not its class. Returns a tuple of budget.Entry, which concatenates onto a Budget's entries.

ParameterUnitPlaneDefaultDescription
v_aSNUinputrequiredVA>0, else ValueError. Read by every model except raman and rayleigh, which are absolute photon fluxes.
t——requiredT∈(0,1], else ValueError. Divides the two Bob-plane fluxes and nothing else. On a link with a loss chain pass TlaunchTspan, as q.Link does: raman and rayleigh are born inside the span.
lengthkm—NoneFibre length. Required by coexist and probe, else ValueError.
alphadB/km—0.2Fibre attenuation, converted to nepers inside both scattering integrals.
symbols—NoneSymbol period τ, converting returned power into photons per detection mode. Required by probe.
coexist—BobNoneA Coexistence. Emits raman, divided by T.
probe—BobNoneA Backscatter. Emits rayleigh, divided by T.
dephase—inputNoneA Dephasing. Emits dephasing, not divided. Its form picks the expression and the plane_note.
pol—BobNoneA Polarisation. Emits polarisation, the noise half alone, referred by Teff (the other half). Reads drift; dispersion reaches security().
modulator—BobNoneA Modulator. Emits imbalance, input-referred once Tηe is stripped. Reads ratio and angle; extinction reaches security().
clock—BobNoneA Timing. Emits timing, the noise half alone, referred by Teff.
python
from qkd import budget, impairments as im

base = budget.assemble(v_a=5.0, t=10**-0.5, v_err=2e-3)
extra = im.assemble_extra(
    v_a=5.0, t=10**-0.5, length=25.0,
    dephase=im.Dephasing(linewidth=10e3, delay=1e-6),
    modulator=im.Modulator(ratio=0.98, angle=0.01),
    pol=im.Polarisation(drift=0.02),
)

full = budget.Budget(entries=base.entries + extra, T=10**-0.5)
full.total    # 0.335494

security() ​

Everything that is not an excess noise, as a plain {name: value} dict on a separate return path. A None descriptor is omitted. Operating points arrive through **kw; a model missing its operating point is skipped, not defaulted.

ParameterKindDefaultDescription
backflashdescriptorNoneA Backflash. Emits backflash_leak and backflash_rate.
deaddescriptorNoneA DeadTime. Emits click_rate if rate is given, and gain with qber if mu is given.
modulatordescriptorNoneA Modulator. Emits extinction when its extinction field is not None.
poldescriptorNoneA Polarisation. Emits visibility, and dgd when dispersion is positive and length is given. visibility is cos⁡σ at the rms drift angle, not averaged over the drift distribution: the contrast at a typical mismatch, not the mean contrast.
siftoperating point1.0Sifted fraction Psift, scaling both backflash outputs.
qberoperating point0.0Observed error rate, entering backflash_rate through h(e) and extinction as the error the leak adds to.
foperating point1.16Error-correction inefficiency charged by backflash_rate. Not the source's 1.15 — see above.
rateoperating pointomittedIncident click rate R∗ in Hz, before dead-time losses.
muoperating pointomittedMean photon number per pulse at the detector.
etaoperating point1.0Detection efficiency, used only by the afterpulse model.
darkoperating point0.0Dark-count probability per gate pDC.
edetoperating point0.0Static detector misalignment error edet, distinct from the afterpulse-induced error the model adds.
lengthoperating pointomittedFibre length in km, needed by dgd alone.

Possible keys: backflash_leak, backflash_rate, click_rate, gain, qber, extinction, visibility, dgd. An absent key means its descriptor or operating point was not supplied, never that the quantity is zero.

catalogue() ​

catalogue() lists every model as (name, callable, parameters): the ξ contributors, then the observables, in the tables' order. A model not listed is unreachable. Each ξ function returns an input-referred value in SNU.

FunctionArgumentsReturns
raman_photons(power, length, ...)P in W, L in km⟨N⟩ per detection mode, at Bob
raman(power, length, t, ...)as above plus Tξ
rayleigh_fraction(length, ...)L in kmreturned power fraction fbs, dimensionless
rayleigh_photons(power, length, symbol, ...)P in W, L in km, τ in s⟨n⟩ per detection mode, at Bob
rayleigh(power, length, symbol, t, ...)as above plus Tξ
phase_variance(linewidth, delay)Δν in Hz, t in sVϕ=2πΔνt in rad²
coherence(linewidth, delay)as abovee−Vϕ/2, the beat note's coherence factor
dephasing(v_a, linewidth, delay)VA in SNU, Δν in Hz, t in sξ
pol_fading(drift)σ in rad(⟨η⟩2,Varη)
polarisation(v_a, drift)VA in SNU, σ in radξ
imbalance(v_a, ratio, angle)VA in SNU, d, θm in radξ
jitter_fading(jitter, width)both in s(⟨η⟩2,Varη)
fading_factor(pol=…, clock=…)the two fading descriptorsthe product of their ⟨η⟩2, 1.0 where neither is given
timing(v_a, jitter, width)VA in SNU, both in sξ
backflash_leak(prob, sift)Pb, Psiftleaked fraction of the sifted key
backflash_rate(sift, prob, qber, f)Psift, Pb, e, fsecure fraction, clamped at zero
visibility(angle)Φ in radfringe contrast cos⁡Φ
dgd(dispersion, length)DPMD in s/km, L in kmmean differential group delay in s
extinction(ratio, qber)r, eQBER including the leaked admixture
saturate(rate, dead, paralysable)R∗ in Hz, τd in sdetected rate in Hz
afterpulse(mu, eta, dark, prob, edet)μ, η, pDC, pAP, edet(Qμ,Eμ)
python
im.phase_variance(20e3, 10e-9)   # 0.00125664  rad^2 over one 100 MBaud symbol
im.dephasing(5.0, 20e3, 10e-9)   # 0.00628121  SNU, input-referred
im.polarisation(5.0, 0.02)       # 4.0e-07     20 mrad rms drift
im.rayleigh_fraction(25.0)       # 0.00076558  == -31.2 dB over 25 km
im.dgd(0.1e-12, 100.0)           # 1e-12       1 ps of DGD over 100 km

6.3×10−3 SNU from two 10 kHz lasers over one 100 MBaud symbol period is the order of the measured envelope. Linewidth and delay enter Vϕ symmetrically.

fading_factor returns a transmittance, not a ξ, and is not in catalogue().

backflash_leak, backflash_rate and afterpulse require a probability in [0,1], extinction a positive ratio, saturate a non-negative dead time. The rest accept any input: an unphysical drift angle or a negative jitter returns a number, not an exception.

q.Link(..., impairments=[...]) matches descriptors onto assemble_extra's keywords by type. An unrecognised object raises, as does a repeated type, e.g. two Dephasing entries describing one laser twice.

python
import qkd as q

res = q.Link(
    modulation=q.GaussianModulation(v_a=5.0),
    channel=q.Fiber(length=25.0, alpha=0.2),
    bob=q.Bob(detector=q.Heterodyne(eta=0.6, v_el=0.1, trusted=True)),
    impairments=[q.Polarisation(drift=0.02), q.Modulator(ratio=0.98)],
).run()

res.explain["xi_polarisation"]   # {'value': 4.0e-07,   'label': 'derived'}
res.explain["xi_imbalance"]      # {'value': 0.001,     'label': 'derived'}
res.explain["xi_total"]          # {'value': 0.0010004, 'label': 'derived'}

xi_total is the sum that reached the covariance. The pinning labels apply unchanged; an impairment row is never a default.

A descriptor moves the rate on both paths ​

The example above is the closed form. On the sampled path no descriptor is simulated into the symbols; its rows are added to the estimate (Measure once, claim many times).

python
def link(impairments=()):
    return q.Link(
        modulation=q.GaussianModulation(v_a=5.0),
        alice=q.Alice(laser=q.Laser(linewidth=10e3),
                      pilots=q.Pilots(power_db=12.0, freq=180e6),
                      symbol_rate=100e6),
        channel=q.Fiber(length=25.0, alpha=0.2),
        bob=q.Bob(detector=q.Heterodyne(eta=0.6, v_el=0.1, trusted=True),
                  lo=q.LocalLO(linewidth=10e3)),
        dsp=q.DSP(phase=q.PilotPhase(), block=32),
        impairments=impairments,
    )

bare = link().run(symbols=1_000_000, seed=1)
res = link([q.Modulator(ratio=1.02, angle=0.01)]).run(symbols=1_000_000, seed=1)

res.explain["xi_imbalance"]["value"]   # 0.0012549978750070725, the descriptor's own
                                       #   row — a closed form in (V_A, d, theta),
                                       #   with no seed and no estimate in it
res.explain["xi_claimed"]              # the clamped estimate PLUS that row
res.explain["xi_total"]                # the assembled budget, beside it

res.explain["xi_claimed"]["value"] > res.est.xi     # True
res.key_rate < bare.key_rate                        # True

Read the ordering, not the magnitude. ξ^ scatters over seeds by more than a small descriptor contributes: at 105 symbols and 10 kHz linewidths, by several times the charged row. The seed-independent claim is xi_imbalance.

The split holds in both directions, since threshold detection has no quadrature variance for an excess noise to inflate:

python
q.Link(..., modulation=q.GaussianModulation(), impairments=[q.Backflash(prob=0.09)])
# ValueError: no Gaussian-modulation term consumes Backflash

q.Link(..., modulation=q.DifferentialPhase(), impairments=[q.Dephasing(...)])
# ValueError: no click bound consumes Dephasing: it yields a Gaussian excess
#             noise, which threshold detection never sees

budget.phase(v_err) and impairments.dephasing(linewidth, delay) are one mechanism at two idealisations: the residual an estimator leaves at finite pilot power, and the diffusion no estimator removes. Where a DSP chain produced a residual, Link adds only that residual and checks it against the diffusion floor Vϕ=2πΔνt:

python
q.Link(..., dsp=q.DSP(phase=q.PilotPhase(v_err=4e-4)),
       impairments=[q.Dephasing(linewidth=20e3, delay=1e-8)]).run()
# ValueError: v_err 0.0004 rad^2 sits below the 0.001257 rad^2 the declared
#             linewidth diffuses over that delay: no estimator removes a phase
#             that has already diffused

The floor is returned as explain["v_floor"], not summed. No xi_dephasing row is emitted on that path, and the check asserts only that the two are ordered.

Detector memory is described either on q.ClickDetector(dead_time=…, afterpulse=…), consumed slot by slot by the simulated click train, or through a DeadTime descriptor on a closed-form path. Never both; Link names the conflict.

Reports: Impairments · Optical · Impairments · Detector.

qkd.attacks ​

Published attacks on the same hardware, in a separate module on a separate return path.

python
from qkd import attacks as at
AttackDescriptorWhat Eve exploitsFamily
saturationat.Saturation(alpha, delta, gain)a homodyne's finite linear range, and that no CV-QKD estimator monitors the quadrature meanquadrature
calibrationat.Calibration(ratio, resend)that every CV-QKD number is quoted in shot-noise units Bob calibrated himselfquadrature
blindingat.Blinding(always, never, passive)that a bright-light-held APD answers classical power and nothing elsethreshold
timeshiftat.Mismatch(hi, lo)that Bob's two detectors are not equally efficient at the same instantthreshold
blankingat.Blanking(blind, signal, gap)the dead time q.DeadTime describes — spent on security here, at no cost in ratethreshold
oscillatorat.Oscillator(monitored, sampled, slope, floor, resend)that a transmitted oscillator sets the shot-noise unit Bob divides by, and that his monitor reads the pulse's peak rather than his sampling instantquadrature
injectionat.Injection(reflect, isolator, stages, atten, bandpass, injected, clock)that Alice's phase modulator back-reflects the setting she just applied — a Trojan-horse probe crosses it twice and disturbs no statistic Alice and Bob can formsource

Descriptors are frozen dataclasses validated by their consumers. at.catalogue() lists the attacks as (name, callable, parameters). at.assess(sat=…, calib=…, blind=…, mismatch=…, blank=…, lo=…, probe=…, **operating_point) returns {name: Reading}, None omitting a row. probe= requires y1 in the operating point and raises without it: the phase-error price is written on the single-photon yield.

oscillator is calibration's arithmetic with the ratio derived, not dialled. q.TransmittedLO's tlo_calib turns the monitored power, the sampled power and Bob's fitted line Z(P)=slopeP+floor into the assumed-over-actual ratio; the two attacks agree wherever the ratios do. It is a separate descriptor because a transmitted oscillator is a security model, not a hardware description.

injection is the source-side attack, the only one not touching Bob. probe_isolation reads a chain of positive dB suppressions — the insertion losses q.Connector, q.Splice and q.Coupling carry — counting the filter and the attenuator twice, since the probe crosses both ways, and the reflection once. probe_photons converts that to photons per modulator setting, probe_delta to a coin imbalance, probe_phase to a phase error rate. The dB hold at one wavelength; qkd's component losses carry none, and Eve chooses the wavelength. probe_rate raises rather than returning a number.

A Reading is two books, and neither is a key rate ​

python
r = at.sat_break(v_a=18.5, t=0.302, eta=0.606, v_el=0.041,
                 alpha=20.0, delta=18.867, xi=0.005)

r.observed     # {'t': 0.146963, 'xi': 0.0049667}
r.eve          # {'xi': 2.005, 'resend': 1.0}

observed is what Alice and Bob's monitoring reports during the attack; eve is what the eavesdropper does. Here the reported ξ is the link's own 0.005 while Eve's full intercept-resend adds 2+ξsys=2.005 SNU: entanglement breaking at every distance, and the noise both source papers quote as undistillable by any CV-QKD link.

These names raise instead of returning a rate:

python
r.key_rate
# AttributeError: a Reading has no key rate: observed and eve are not the same
#   quantity, and the first does not bound the second -- under every attack
#   modelled here the observables stay at values an unattacked link would
#   produce. Read Reading.observed and Reading.eve separately, and never
#   subtract them

key_rate, rate, key, secure, safe and margin raise that message; any other missing attribute raises the ordinary AttributeError. The gap between the books is not a security margin (Security). r.table() prints both books side by side with the sentence naming why neither bounds the other.

None of these ξ values may enter a budget ​

at.sat_estimate and at.calib_xi return SNU numbers spelled ξ that must not reach budget.Entry: they are estimator artefacts, not light in a fibre, and belong to no plane.

python
at.sat_estimate(18.5, 0.302, 0.606, 0.041, alpha=20.0, delta=0.0,  xi=0.005)
# (0.302000, 2.005000)     no displacement: the intercept-resend is in plain sight

at.sat_estimate(18.5, 0.302, 0.606, 0.041, alpha=20.0, delta=18.867, xi=0.005)
# (0.146963, 0.004967)     displaced into the clip: xi reads honest, T-hat does not

The calibration attack acts through the unit instead of the estimator. The overestimate erasing a given excess noise has a closed form:

python
at.calib_ratio(0.01, 0.5, 0.6)          # 1.003    the N'_0/N_0 that reports xi = 0
at.calib_xi(0.01, 1.003, 0.5, 0.6)      # 3.6e-16  and it does

calib_xi returns negatives raw. A negative apparent excess noise is the attack's signature; clamping would erase it.

The threshold-detector three ​

python
at.shift_ratio(hi=0.2, lo=0.02)   # 0.100000  the efficiency mismatch r = min/max
at.shift_qber(0.1)                # 0.153846  what a faked-state attack induces
at.shift_leak(0.1)                # 0.560503  Eve's information, per sifted bit
at.shift_bound(0.1)               # 0.439497  the ceiling any rate must sit under

The time-shift attack induces no error at any mismatch: Eve reroutes the pulse and never measures it. shift_break therefore reports the QBER unmoved beside the naive rate a mismatch-blind proof would claim. at.mismatch_rate(hi, lo, e_bit, e_phase) is what Alice and Bob are entitled to once the mismatch is accounted for. Its e_phase has no default: equating it to e_bit assumes a channel symmetric between the bases, which an efficiency mismatch is not.

Two of the detector attacks refuse rather than approximate. Blinding refuses when the trigger powers do not separate the detectors:

python
at.blind_break(gain=0.01, qber=0.01, always=(2.5e-3,), never=(1.0e-3,))
# ValueError: max(P_always)/min(P_never) = 2.5 is not below 2, so Lydersen
#   arXiv:1008.4593 Eq. (1) fails: a pulse able to fire the intended detector also
#   fires at least one other, and the attack leaves errors. This module has no model
#   for that partial control and will not approximate one

Blanking refuses a pulse timed outside the window it must hide in: inside Bob's acceptance window it is counted, older than the dead time it is harmless.

python
at.blank_hidden(gap=5e-9,   window=1e-9, dead=50e-9)   # True
at.blank_hidden(gap=100e-9, window=1e-9, dead=50e-9)
# ValueError: a blinding pulse 1e-07 s ahead of the slot is older than the 5e-08 s
#   dead time it has to survive: the detectors have recovered by the time Alice's
#   photon arrives and nothing is blinded

blank_leak(16.52, 0.1) returns 0.930949 bit per sifted bit at the source's blinding intensity. Two readings of the source's equations are defensible. The module takes the literal one, which sits above the source's measured figure; blank_leak's docstring states both readings and their numbers. Verification here reproduces the published algebra, not a measured constant — see Validation.

Reports: Attacks · Continuous · Attacks · Threshold · Attacks · Contract.

References ​

KeySource
Antonelli 2024Antonelli et al., arXiv:2408.01754
Huang 2012Huang, Yin, Wang, Li, Chen & Han, arXiv:1206.6591
Kish 2024Kish et al., Quantum 8, 1382 (2024)
Krause 2025Krause et al., arXiv:2507.10361
Kumar 2015Kumar, Qin & Alléaume, New J. Phys. 17, 043027 (2015), arXiv:1412.1403
Laudenbach 2018Laudenbach et al. 2018, arXiv:1703.09278
Lütkenhaus 2000Lütkenhaus, Phys. Rev. A 61, 052304 (2000)
Mandil 2024Mandil, Qian & Lo, arXiv:2407.08009
Marie & Alléaume 2017Marie & Alléaume, PRA 95, 012316 (2017)
Papapanos 2020Papapanos et al., arXiv:2010.03358
Qi 2015Qi et al., PRX 5, 041009 (2015), arXiv:1503.00662
Rogers 2007Rogers et al., New J. Phys. 9, 319 (2007), arXiv:0706.1449
Sharma 2024Sharma et al., arXiv:2409.05802
Singh 2025Singh et al., IEEE Photonics J. 17, 7600206 (2025), arXiv:2502.04081
Subacius 2005Subacius, Zavriyev & Trifonov, Appl. Phys. Lett. 86, 011103 (2005)
Usenko 2012Usenko et al., New J. Phys. 14, 093048 (2012), arXiv:1208.4307
Wang 2025Wang et al., arXiv:2503.10168
Xu 2013Xu et al., New J. Phys. 15, 113007 (2013)
Yuan 2014Yuan et al., PRApplied 2, 064006 (2014)

Cited above without an identifier here: Meda, Li, Diamanti, Ma & Lo.