Impairments
Noise sources outside Laudenbach's Section 9, and click-protocol observables that are not excess noise. Companion to qkd.budget.
from qkd import impairments as imAn 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 q.DeadTime and a blanking attack's hiding place.
Descriptors are frozen dataclasses. 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.
| Model | Formula | Stated at | Referred by | Source |
|---|---|---|---|---|
raman | Bob | Kumar 2015, Eqs. (6)–(7) | ||
rayleigh | Bob | Mandil 2024, Eq. (A1), into Laudenbach Eqs. (9.59)–(9.60) | ||
dephasing | budget.phase at | input | not divided | Qi 2015, Eqs. (9)–(10) |
polarisation | Bob | Usenko 2012, unnumbered; overlap from Sharma 2024 | ||
imbalance | Bob | Wang 2025, Eq. (E1) | ||
timing | Bob | Usenko 2012, unnumbered, on a Gaussian matched-filter overlap |
polarisation and timing are fading channels: Usenko splits each into a fixed channel of transmittance
A fading row is two halves
| Half | Where it is | Reaches |
|---|---|---|
| the noise half, | polarisation(), timing(), and the polarisation/timing rows of assemble_extra() | the budget, as a budget.Entry |
| the transmittance half, | pol_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:
so charging the noise half against an unattenuated 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:
| Observable | Returns | Observed at | Source |
|---|---|---|---|
backflash_leak | Eve's tap on the fibre outside Bob | Singh 2025 | |
backflash_rate | the sifted key | Singh 2025 | |
extinction | Bob's sifted bits | Huang 2012, Eqs. (1) and (5) | |
visibility | the receiver's interference | Sharma 2024 | |
dgd | the fibre span | Antonelli 2024 | |
saturate | detected click rate after dead-time losses | Bob's detector | Krause 2025, Eq. (1); Rogers 2007 |
afterpulse | Bob's detector | Papapanos 2020, Eqs. (4), (7) and (8) |
The default is not the source's
backflash_rate charges q.IndividualAttack's default. Singh 2025 write "the usual value for 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
| Status | Means |
|---|---|
| published | the closed form and its constants come from the cited source |
| qkd connects | the 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 input | the parameter is a measurement of one device, with no model predicting it from anything more primitive |
| diagnostic | computed and reported, never converted into a |
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.
| Mechanism | Status | Published | qkd's own step |
|---|---|---|---|
Raman (raman) | published, on a measured input | Kumar Eqs. (6) and (7): the SASRS photon number for both launch geometries, and its conversion to a channel-input | 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, Coexistence.beta takes the measurement (gap 1). |
Rayleigh (rayleigh) | qkd connects | Mandil Eq. (A1), the fibre's impulse response | The round-trip integral of that response for a continuous-wave launch, 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) | published | Qi Eqs. (9)–(10): | Composition of two published results. The default estimator form is prior art, not a house derivation. |
Polarisation (polarisation, visibility, dgd) | published; dgd diagnostic | Usenko's fading identity: a channel of fluctuating transmittance is a fixed channel of transmittance | The Gaussian average of the overlap over a drifting angle: |
Modulator (imbalance, extinction) | published | Wang Eq. (E1), | Stripping 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 connects | Usenko's fading identity, unchanged. | The matched-filter overlap jitter_fading's docstring. Only |
Backflash (backflash_leak, backflash_rate) | published, on a measured input | Singh's | Backflash(prob) takes the measurement. The anchor QBER is fitted at |
Dead time (saturate, afterpulse) | published | Non-paralysable | At 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
The gaps
Each open gap is open because the published constant or closed form does not exist.
| # | Gap | Status, and why |
|---|---|---|
| 1 | Spacing dependence of the Raman coefficient | Open. |
| 2 | PMD to excess noise | Open. Converting a DGD into |
| 3 | Extinction ratio to a CV excess noise | Open, 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. |
| 4 | Bias drift beyond the quadrature bias point | Open; reduces to gap 3. Wang's |
| 5 | Open. Every source treats | |
| 6 | The normalisation of Meda's | Open. Their 9.8 % and 6 % assume |
| 7 | Double backscattering on a one-way link | Open. On a one-way link single backscatter travels away from Bob; only the second-order forward term reaches him. It scales as |
| 8 | The two connecting integrals | Not 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. |
| 9 | Composition of budget.phase and impairments.dephasing | Closed. 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 conversion | report the ingredient and stop — the diagnostic status: dgd returns a differential group delay in seconds and no |
| Constant exists only as a measurement | the descriptor takes the measurement ( |
| A number fitted to close an anchor | the 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
| Parameter | Unit | Plane | Default | Description |
|---|---|---|---|---|
Coexistence.channels | — | Bob | required | Co-propagating classical channels. Launch powers add linearly in the Raman source term, so |
Coexistence.launch | dBm | Bob | required | Per-channel launch power. Kumar found a 25 km CV-QKD link tolerates up to |
Coexistence.beta | 1/(km nm) | Bob | 3.0e-9 | Raman coefficient, measured: Kumar report |
Coexistence.wavelength | m | Bob | 1531.12e-9 | Quantum-channel wavelength |
Coexistence.demux | — | Bob | 1.0 | Kumar Eq. (6)'s q.Link refuses a declared value by name. Carried so raman_photons() reproduces the source's Bob-plane figures. |
Coexistence.backward | — | — | False | Counter-propagating classical channels: backward geometry |
Backscatter.power | W | Bob | required | Power of the counter-propagating tone scattering back into Bob: a two-way architecture's outbound pulse train or a classical channel. |
Backscatter.coeff | 1/s | Bob | 8.0 | Returned power at |
Backscatter.index | — | Bob | 1.468 | Fibre group index |
Backscatter.wavelength | m | Bob | 1550.12e-9 | The launch wavelength. Rayleigh scattering is elastic, so the return is in band and no spectral filter applies. |
Polarisation.drift | rad | Bob | required | RMS mismatch angle |
Polarisation.dispersion | s/ | — | 0.0 | PMD coefficient dgd() alone. q.Link refuses a declared value by name: no verified closed form converts a DGD into an excess noise. |
Transmitter
| Parameter | Unit | Plane | Default | Description |
|---|---|---|---|---|
Dephasing.linewidth | Hz | input | required | The sum of both lasers' Lorentzian full widths; the beat phase diffuses at the sum rate. |
Dephasing.delay | s | input | required | Time 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 | — | Bob | 1.0 | I/Q amplitude imbalance |
Modulator.angle | rad | Bob | 0.0 | Quadrature bias-point offset |
Modulator.extinction | — | — | None | Linear extinction ratio 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.jitter | s | Bob | required | RMS sampling-instant error: clock jitter plus residual drift. |
Timing.width | s | Bob | required | Field envelope parameter |
Detector
| Parameter | Unit | Plane | Default | Description |
|---|---|---|---|---|
Backflash.prob | — | — | required | |
DeadTime.dead | s | — | required | Interval |
DeadTime.afterpulse | — | — | 0.0 | Probability |
DeadTime.paralysable | — | — | False | Whether 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 |
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, QBERAt
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.
| Parameter | Unit | Plane | Default | Description |
|---|---|---|---|---|
v_a | SNU | input | required | ValueError. Read by every model except raman and rayleigh, which are absolute photon fluxes. |
t | — | — | required | ValueError. Divides the two Bob-plane fluxes and nothing else. On a link with a loss chain pass q.Link does: raman and rayleigh are born inside the span. |
length | km | — | None | Fibre length. Required by coexist and probe, else ValueError. |
alpha | dB/km | — | 0.2 | Fibre attenuation, converted to nepers inside both scattering integrals. |
symbol | s | — | None | Symbol period probe. |
coexist | — | Bob | None | A Coexistence. Emits raman, divided by |
probe | — | Bob | None | A Backscatter. Emits rayleigh, divided by |
dephase | — | input | None | A Dephasing. Emits dephasing, not divided. Its form picks the expression and the plane_note. |
pol | — | Bob | None | A Polarisation. Emits polarisation, the noise half alone, referred by drift; dispersion reaches security(). |
modulator | — | Bob | None | A Modulator. Emits imbalance, input-referred once ratio and angle; extinction reaches security(). |
clock | — | Bob | None | A Timing. Emits timing, the noise half alone, referred by |
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.335494security()
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.
| Parameter | Kind | Default | Description |
|---|---|---|---|
backflash | descriptor | None | A Backflash. Emits backflash_leak and backflash_rate. |
dead | descriptor | None | A DeadTime. Emits click_rate if rate is given, and gain with qber if mu is given. |
modulator | descriptor | None | A Modulator. Emits extinction when its extinction field is not None. |
pol | descriptor | None | A Polarisation. Emits visibility, and dgd when dispersion is positive and length is given. visibility is |
sift | operating point | 1.0 | Sifted fraction |
qber | operating point | 0.0 | Observed error rate, entering backflash_rate through extinction as the error the leak adds to. |
f | operating point | 1.16 | Error-correction inefficiency charged by backflash_rate. Not the source's 1.15 — see above. |
rate | operating point | omitted | Incident click rate |
mu | operating point | omitted | Mean photon number per pulse at the detector. |
eta | operating point | 1.0 | Detection efficiency, used only by the afterpulse model. |
dark | operating point | 0.0 | Dark-count probability per gate |
edet | operating point | 0.0 | Static detector misalignment error |
length | operating point | omitted | Fibre 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
| Function | Arguments | Returns |
|---|---|---|
raman_photons(power, length, ...) | ||
raman(power, length, t, ...) | as above plus | |
rayleigh_fraction(length, ...) | returned power fraction | |
rayleigh_photons(power, length, symbol, ...) | ||
rayleigh(power, length, symbol, t, ...) | as above plus | |
phase_variance(linewidth, delay) | ||
coherence(linewidth, delay) | as above | |
dephasing(v_a, linewidth, delay) | ||
pol_fading(drift) | ||
polarisation(v_a, drift) | ||
imbalance(v_a, ratio, angle) | ||
jitter_fading(jitter, width) | both in s | |
fading_factor(pol=…, clock=…) | the two fading descriptors | the product of their 1.0 where neither is given |
timing(v_a, jitter, width) | ||
backflash_leak(prob, sift) | leaked fraction of the sifted key | |
backflash_rate(sift, prob, qber, f) | secure fraction, clamped at zero | |
visibility(angle) | fringe contrast | |
dgd(dispersion, length) | mean differential group delay in s | |
extinction(ratio, qber) | QBER including the leaked admixture | |
saturate(rate, dead, paralysable) | detected rate in Hz | |
afterpulse(mu, eta, dark, prob, edet) |
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 kmfading_factor returns a transmittance, not a catalogue().
backflash_leak, backflash_rate and afterpulse require a probability in 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.
Reaching a Link
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.
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).
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 # TrueRead the ordering, not the magnitude. xi_imbalance.
The split holds in both directions, since threshold detection has no quadrature variance for an excess noise to inflate:
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 seesbudget.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
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 diffusedThe 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.
from qkd import attacks as at| Attack | Descriptor | What Eve exploits | Family |
|---|---|---|---|
saturation | at.Saturation(alpha, delta, gain) | a homodyne's finite linear range, and that no CV-QKD estimator monitors the quadrature mean | quadrature |
calibration | at.Calibration(ratio, resend) | that every CV-QKD number is quoted in shot-noise units Bob calibrated himself | quadrature |
blinding | at.Blinding(always, never, passive) | that a bright-light-held APD answers classical power and nothing else | threshold |
timeshift | at.Mismatch(hi, lo) | that Bob's two detectors are not equally efficient at the same instant | threshold |
blanking | at.Blanking(blind, signal, gap) | the dead time q.DeadTime describes — spent on security here, at no cost in rate | threshold |
oscillator | at.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 instant | quadrature |
injection | at.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 form | source |
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
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
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
These names raise instead of returning a rate:
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 themkey_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 budget.Entry: they are estimator artefacts, not light in a fibre, and belong to no plane.
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 notThe calibration attack acts through the unit instead of the estimator. The overestimate erasing a given excess noise has a closed form:
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 doescalib_xi returns negatives raw. A negative apparent excess noise is the attack's signature; clamping would erase it.
The threshold-detector three
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 underThe 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:
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 oneBlanking 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.
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 blindedblank_leak(16.52, 0.1) returns 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
| Key | Source |
|---|---|
| Antonelli 2024 | Antonelli et al., arXiv:2408.01754 |
| Huang 2012 | Huang, Yin, Wang, Li, Chen & Han, arXiv:1206.6591 |
| Kish 2024 | Kish et al., Quantum 8, 1382 (2024) |
| Krause 2025 | Krause et al., arXiv:2507.10361 |
| Kumar 2015 | Kumar, Qin & Alléaume, New J. Phys. 17, 043027 (2015), arXiv:1412.1403 |
| Laudenbach 2018 | Laudenbach et al. 2018, arXiv:1703.09278 |
| Lütkenhaus 2000 | Lütkenhaus, Phys. Rev. A 61, 052304 (2000) |
| Mandil 2024 | Mandil, Qian & Lo, arXiv:2407.08009 |
| Marie & Alléaume 2017 | Marie & Alléaume, PRA 95, 012316 (2017) |
| Papapanos 2020 | Papapanos et al., arXiv:2010.03358 |
| Qi 2015 | Qi et al., PRX 5, 041009 (2015), arXiv:1503.00662 |
| Rogers 2007 | Rogers et al., New J. Phys. 9, 319 (2007), arXiv:0706.1449 |
| Sharma 2024 | Sharma et al., arXiv:2409.05802 |
| Singh 2025 | Singh et al., IEEE Photonics J. 17, 7600206 (2025), arXiv:2502.04081 |
| Subacius 2005 | Subacius, Zavriyev & Trifonov, Appl. Phys. Lett. 86, 011103 (2005) |
| Usenko 2012 | Usenko et al., New J. Phys. 14, 093048 (2012), arXiv:1208.4307 |
| Wang 2025 | Wang et al., arXiv:2503.10168 |
| Xu 2013 | Xu et al., New J. Phys. 15, 113007 (2013) |
| Yuan 2014 | Yuan et al., PRApplied 2, 064006 (2014) |
Cited above without an identifier here: Meda, Li, Diamanti, Ma & Lo.