typed op• Data kinds: counts → counts
• Call: fullseye.apply(img, "tb_spad_deadtime_apply", a=0.5, b=0.5) (the 2-D model is one image plus two scalar knobs a,b∈[0,1])

*The figure is the actual output on a synthetic 128×128 input. Left: input, right: output. Point clouds are drawn as a top-down scatter (brightness = z), 1-D series as a line plot, volumes as the maximum-intensity projection along z, videos as the middle frame, complex images as magnitude; return values that are not pictures are shown as the values themselves.*
Sweeping knob a (0.1 / 0.5 / 0.9, the other knob at its default):
▸ tb_spad_deadtime_apply: knob a sweep (docs site)
*Knob b does not change the output (measured: identical at 0.1 / 0.5 / 0.9).*
Stages (the ops that come before → this op, left to right):
▸ tb_spad_deadtime_apply: stages (docs site)
On other images (synthetic scene / photo / coins. Top row: inputs, bottom row: their outputs. Knobs at default):
▸ tb_spad_deadtime_apply: other inputs (docs site)
Distort a true photon rate by the detector's dead time (counts lost).
After every detection a SPAD is blind for a recharge (dead) time `tau`, so
the *measured* rate `m is always below the *true* incident rate n`.
Two classical laws, and this op implements both:
• non-paralysable (default) — an arriving photon during the dead time is
simply lost: `m = n / (1 + n*tau). Monotonic, saturating at 1/tau`.
• paralysable (`paralyzable=True`) — an arriving photon *restarts* the
dead time: `m = n * exp(-n*tau). This law **peaks** at n = 1/tau`
(where `m = 1/(e*tau)`) and then falls, so a bright scene can read
*darker* than a dim one. That is why no inverse op exists for it (see
:func:spad_deadtime_correct).
*rate_hz* is a 1-D array of true rates in counts per second; *dead_time_ns*
is the dead time in nanoseconds, defaulting to 50 — the middle of the
10-100 ns range a passively quenched SPAD occupies, and a placeholder to be
replaced by the datasheet value, never a measurement of your detector.
Returns the measured rates as a float64 1-D array of the same length.
Ground truth (pinned in the tests): at `n = 1/tau` the non-paralysable law
gives exactly `n/2`; the paralysable law's maximum is exactly
`1/(e*tau) at n = 1/tau; both reduce to m = n as n*tau -> 0`.
Raises `ValueError`: negative, non-finite or non-1-D *rate_hz*, a
non-positive *dead_time_ns*, and a non-bool *paralyzable*.
Typed bridge of the photon op `spad_deadtime_apply into the 2-D evolution registry: the same implementation, called under the op(v, a, b) convention. a drives dead_time_ns (default 50); b` is unused.
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
The program below has been verified to run (same input as the figure). In Studio's help this block becomes buttons that load and run it on the spot.
img_to_counts 0.50 0.50 tb_spad_deadtime_apply 0.50 0.50
▸ Load this pipeline · Load & run
The examples below call the underlying ledger op spad_deadtime_apply. This bridge op is the same implementation adapted to the fn(v, a, b) convention, so the behaviour carries over unchanged (only the call form differs).
• photon_timeresolved — py -3.11 examples/photon_timeresolved.py
counts as input)identity · tb_spad_deadtime_correct · tb_tcspc_coates_correct · tb_tcspc_irf_convolve · tb_tcspc_background_subtract · tb_dtof_depth · tb_countrate_to_counts · tb_counts_to_countrate
typed)tb_points_to_voxel · tb_estimate_point_normals · tb_iss_keypoints · tb_project_points · tb_render_point_depth · tb_statistical_outlier_removal · tb_radius_outlier_removal · tb_voxel_grid_downsample
*Provenance: ops.py — 2D operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.