Optics and PSF¶
Conventions¶
- The
ZStageposition is the focal plane height; an emitter's defocus is \(z_{emitter} - Z\). Positive \(Z\) moves focus up through the sample, like a real focus drive. ForCellField, \(Z = 0\) puts the coverslip in focus (the stage starts at 0.5 µm). - The camera pixel size and the PSF are independent; the PSF kernel is oversampled and placed at the emitter's true
sub-pixel position. In Micro-Manager the pixel size is the camera's sensor pixel / (objective magnification x
EmissionPathmagnification): 6.5 µm / (100 x 0.667) = 97.45 nm by default (Kinetix22, 100x objective; the 6.5 µm pixel from [kinetix-datasheet], the 0.667 an estimate). The cli and viewer take it directly (pixel-nm, default 100).
PSF models (adapter)¶
Renderer.PsfModel |
What it is |
|---|---|
Gaussian |
\(\sigma = 0.21\lambda/\mathrm{NA}\) [zhang2007]; no defocus law yet |
RichardsWolf |
Vectorial focus field [richards1959], in-focus, no index mismatch (PSFGenerator [kirshner2013]) |
GibsonLanni |
Adds the sample/immersion index mismatch and depth [gibson1992] (PSFGenerator [kirshner2013]) |
GibsonLanniZernike (default) |
Scalar Gibson-Lanni [gibson1992] plus a pupil wavefront from OSA/ANSI Zernike modes 0-27 [thibos2002] (this project, C++, chirp-Z; the same model as webSMLM) |
GibsonLanniZernike is a scalar model: a unit-amplitude pupil (no apodization, no polarization) whose phase is the
Gibson-Lanni optical path difference (sample depth and immersion defocus), the Zernike sum and an optional mask. It is the
same model as webSMLM's, computed in C++ (Simulation/ZernikePsf.cpp), identical to webSMLM's JavaScript to the last
float32 bit on the reference cases at webSMLM's 64-sample pupil (ctest zernike_psf; the default grid is finer, below) and to the original Java class (GibsonLanniZernikePSF.java, kept
as a reference) to ~1e-12 relative L2. It needs no Java. RichardsWolf and GibsonLanni run in PSFGenerator's JVM and only in a DLL built with
PSFGenerator (BUILDING.md); the released DLL has
neither.
The default PSF next to the unaberrated one, as the movie's kernel holds it:
How this figure was made (2 cli runs, 569 ms)
Generated by tools/build_physics_figures.py (figure psf-sections) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --psf-out no.psf --psf-kernel-half-width-nm 2000 --psf-z-range-um 4 --psf-zernike-preset None --npc-dye None # 299 ms [served]
insiliscope_cli --psf-out default.psf --psf-kernel-half-width-nm 2000 --psf-z-range-um 4 --npc-dye None # 270 ms [served]
The Zernike model evaluates the pupil-to-image integral on a Cartesian pupil grid with a separable 2D chirp-Z (Bluestein)
transform [rabiner1969, bluestein1970]. The grid
has as many samples as the kernel window needs (Renderer.PsfPupilSamples, cli/viewer psf-pupil-samples, 0 =
automatic): a sampled pupil makes the PSF repeat every \((M-4)\,\lambda/(2\,\mathrm{NA})\), so the
period must cover the window's diagonal, and the count is then raised to fill the transform's power-of-two length (free).
The default +/-7 µm window gets 184 x 184 samples, the viewer's +/-3 µm one 152.
Pupil sampling before 2026-10-07
The grid used to be 64 x 64 (webSMLM's PSF_FFT_M). Its period, ~14.6 µm in the red and ~10.9 µm in the green at
NA 1.4, was shorter than the 14 µm wide kernel, so defocused light folded back into the window: WideField images
showed stepped, square-ish bands of out-of-focus light (up to 3-4 sigma per frame), and near focus a camera pixel was
off by up to 0.3 % of the emitter's photons (1.5 % of the brightest pixel). Against a 768-sample reference the
automatic grid is within 1e-4 of an emitter's photons per camera pixel in the core and 1.3e-5 beyond 2 µm.
psf-pupil-samples=64 gives the old grid (and webSMLM's PSF, to the last bit).
The pupil grid measured against a finer one:
| 64 (webSMLM) 64 samples |
Automatic (default) 184 samples |
512 (reference) 512 samples |
|
|---|---|---|---|
| 3 µm out of focus (log, 6 decades) | ![]() |
![]() |
![]() |
| Max error per pixel (share of photons) | 6.6×10-4 | 5.2×10-5 | 0 |
| ... beyond 2 µm defocus | 7.4×10-5 | 1.4×10-5 | 0 |
| Kernel time | 264 ms | 311 ms | 670 ms |
The pupil grid of the chirp-Z evaluation (Renderer.PsfPupilSamples, cli psf-pupil-samples) on the default ±7 µm kernel (planes every 0.5 µm here). A sampled pupil makes the PSF repeat every (M-4)λ/(2NA): with 64 samples that period is shorter than the window and defocused light folds back in (the square pattern around the defocused spot). Images: a 1-photon emitter 3 µm out of focus on the camera. Errors: against the 512-sample reference, over all planes. Times: the kernel computed in a fresh process (the whole cli run), on the build machine (4 threads).
How this figure was made (3 cli runs, 1.24 s)
Generated by tools/build_physics_figures.py (figure psf-pupil) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time:
insiliscope_cli --psf-out M64.psf --psf-z-range-um 7 --psf-z-step-um 0.5 --psf-pupil-samples 64 --npc-dye None # 264 ms
insiliscope_cli --psf-out M0.psf --psf-z-range-um 7 --psf-z-step-um 0.5 --psf-pupil-samples 0 --npc-dye None # 311 ms
insiliscope_cli --psf-out M512.psf --psf-z-range-um 7 --psf-z-step-um 0.5 --psf-pupil-samples 512 --npc-dye None # 670 ms
Other pieces: the Gibson-Lanni focal shift for depth (paraxial: the z stack is centred at \(t_{i0} - d\,n_i/n_s\)),
and presets of aberrations (astigmatism, coma, spherical, trefoil, saddle point, extended-range engineered PSFs) taken
from webSMLM (the single-mode amplitudes, 0.07-0.3 waves, and the default MixedRealisticObjective mix are
order-of-magnitude estimates, not values measured on an objective).
Every preset, its pupil wavefront and its PSF through focus:
| Preset | Pupil phase (waves) | Wavefront (waves) | x-z (log) | z = -0.6 µm | z = 0 | z = +0.6 µm |
|---|---|---|---|---|---|---|
| None | ![]() ±0.3 |
0 | ![]() |
![]() |
![]() |
![]() |
| AstigmatismWeak | ![]() ±0.3 |
rms 0.029 peak 0.069 |
![]() |
![]() |
![]() |
![]() |
| AstigmatismModerate | ![]() ±0.3 |
rms 0.061 peak 0.15 |
![]() |
![]() |
![]() |
![]() |
| AstigmatismStrong | ![]() ±0.3 |
rms 0.12 peak 0.30 |
![]() |
![]() |
![]() |
![]() |
| ComaWeak | ![]() ±0.3 |
rms 0.035 peak 0.099 |
![]() |
![]() |
![]() |
![]() |
| ComaStrong | ![]() ±0.3 |
rms 0.089 peak 0.25 |
![]() |
![]() |
![]() |
![]() |
| SphericalWeak | ![]() ±0.3 |
rms 0.045 peak 0.10 |
![]() |
![]() |
![]() |
![]() |
| SphericalStrong | ![]() ±0.3 |
rms 0.11 peak 0.25 |
![]() |
![]() |
![]() |
![]() |
| TrefoilModerate | ![]() ±0.3 |
rms 0.053 peak 0.15 |
![]() |
![]() |
![]() |
![]() |
| MixedRealisticObjective | ![]() ±0.3 |
rms 0.058 peak 0.20 |
![]() |
![]() |
![]() |
![]() |
| SaddlePoint | ![]() ±3 |
rms 0.25 peak 0.78 |
![]() |
![]() |
![]() |
![]() |
| ExtendedRange | ![]() ±3 |
rms 0.43 peak 1.4 |
![]() |
![]() |
![]() |
![]() |
| ExtendedRangeStrong | ![]() ±3 |
rms 0.78 peak 2.8 |
![]() |
![]() |
![]() |
![]() |
Every Zernike preset (Objective.ZernikePreset, cli psf-zernike-preset), default label (678 nm, NA 1.4). Pupil phase: the Zernike wavefront over the pupil, blue negative, red positive, on one scale for every preset (±0.3 waves) so the colours compare their strengths. The engineered SaddlePoint, ExtendedRange and ExtendedRangeStrong (peak above 0.5 wave, made for a longer z range) share a wider scale (±3 waves). Beside it the wavefront's rms and peak. x-z: 2 µm wide, 2.4 µm deep (+z up), x and z at one scale, four decades. x-y planes: 2 µm across, linear, each its own scale. The astigmatic and extended-range presets stretch the spot one way above focus and the other way below (z encoding).
How this figure was made (14 cli runs, 516 ms)
Generated by tools/build_physics_figures.py (figure psf-presets) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --presets-json presets.presets.json --npc-dye None # 2.3 ms [served]
insiliscope_cli --psf-out None.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset None --npc-dye None # 33 ms [served]
insiliscope_cli --psf-out AstigmatismWeak.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset AstigmatismWeak --npc-dye None # 36 ms [served]
insiliscope_cli --psf-out AstigmatismModerate.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset AstigmatismModerate --npc-dye None # 34 ms [served]
insiliscope_cli --psf-out AstigmatismStrong.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset AstigmatismStrong --npc-dye None # 31 ms [served]
insiliscope_cli --psf-out ComaWeak.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset ComaWeak --npc-dye None # 30 ms [served]
insiliscope_cli --psf-out ComaStrong.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset ComaStrong --npc-dye None # 38 ms [served]
insiliscope_cli --psf-out SphericalWeak.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset SphericalWeak --npc-dye None # 37 ms [served]
insiliscope_cli --psf-out SphericalStrong.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset SphericalStrong --npc-dye None # 36 ms [served]
insiliscope_cli --psf-out TrefoilModerate.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset TrefoilModerate --npc-dye None # 35 ms [served]
insiliscope_cli --psf-out MixedRealisticObjective.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset MixedRealisticObjective --npc-dye None # 35 ms [served]
insiliscope_cli --psf-out SaddlePoint.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset SaddlePoint --npc-dye None # 46 ms [served]
insiliscope_cli --psf-out ExtendedRange.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset ExtendedRange --npc-dye None # 63 ms [served]
insiliscope_cli --psf-out ExtendedRangeStrong.psf --psf-kernel-half-width-nm 1500 --psf-z-range-um 2.4 --psf-z-step-um 0.1 --psf-zernike-preset ExtendedRangeStrong --npc-dye None # 62 ms [served]
Placement: Nearest (box average), Linear, Cubic (Catmull-Rom [catmull1974],
default) or Fft (Fourier shift, slow) sample the
oversampled kernel at the true emitter position. Kernel planes are normalised to sum 1 with precomputed block sums, so each
camera pixel is one interpolation.
The four placements against the exact Fourier shift:
| Fft exact; CPU only, slow: the reference |
Cubic default (CPU and GPU) |
Linear CPU and GPU |
Nearest snaps to the kernel's 1/6 px grid |
|
|---|---|---|---|---|
| Blink at +0.3 px (9 × 9 px) | ![]() |
![]() |
![]() |
![]() |
| Difference to Fft (±0.0061 of its photons) | exact | ![]() |
![]() |
![]() |
| Max difference (share of photons) | 0 | 9.9×10-6 | 5.3×10-4 | 0.0061 |
| Render time per blink | 559 µs 95 blinks |
37 µs 2406 blinks |
12 µs 2406 blinks |
6.5 µs 2406 blinks |
Renderer.PsfInterp, cli psf-interp) samples the oversampled kernel at the emitter's sub-pixel position (default label, in focus, halo cut off). Fft shifts the kernel by a Fourier phase: exact for the band-limited kernel, but slow and CPU only (Micro-Manager renders on the CPU when it is chosen), so it serves as the reference here. Cubic (Catmull-Rom [catmull1974], the default everywhere: Micro-Manager, cli, viewer) and Linear interpolate between kernel samples; Nearest snaps to the kernel's 1/6-pixel grid. The curve: each method's 9 × 9 px centroid minus Fft's at the same offset (100 nm pixels); Nearest is off this scale (up to 7.5 nm). Times: the default DNA-PAINT movie (Fft: one 16 px frame), per blink, on the build machine (4 threads).How this figure was made (28 cli runs, 1.11 s)
Generated by tools/build_physics_figures.py (figure psf-interp) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --splat-out Fft-0.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0,0,0 --npc-dye None # 12 ms [served]
insiliscope_cli --splat-out Fft-0.1.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0.1,0,0 --npc-dye None # 4.3 ms [served]
insiliscope_cli --splat-out Fft-0.2.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0.2,0,0 --npc-dye None # 4.1 ms [served]
insiliscope_cli --splat-out Fft-0.3.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0.3,0,0 --npc-dye None # 4.1 ms [served]
insiliscope_cli --splat-out Fft-0.4.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0.4,0,0 --npc-dye None # 4.1 ms [served]
insiliscope_cli --splat-out Fft-0.5.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Fft --splat-at 0.5,0,0 --npc-dye None # 4.1 ms [served]
insiliscope_cli --splat-out Cubic-0.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0,0,0 --npc-dye None # 1.3 ms [served]
insiliscope_cli --splat-out Cubic-0.1.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0.1,0,0 --npc-dye None # 292 µs [served]
insiliscope_cli --splat-out Cubic-0.2.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0.2,0,0 --npc-dye None # 297 µs [served]
insiliscope_cli --splat-out Cubic-0.3.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0.3,0,0 --npc-dye None # 290 µs [served]
insiliscope_cli --splat-out Cubic-0.4.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0.4,0,0 --npc-dye None # 307 µs [served]
insiliscope_cli --splat-out Cubic-0.5.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Cubic --splat-at 0.5,0,0 --npc-dye None # 282 µs [served]
insiliscope_cli --splat-out Linear-0.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0,0,0 --npc-dye None # 294 µs [served]
insiliscope_cli --splat-out Linear-0.1.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0.1,0,0 --npc-dye None # 278 µs [served]
insiliscope_cli --splat-out Linear-0.2.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0.2,0,0 --npc-dye None # 289 µs [served]
insiliscope_cli --splat-out Linear-0.3.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0.3,0,0 --npc-dye None # 304 µs [served]
insiliscope_cli --splat-out Linear-0.4.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0.4,0,0 --npc-dye None # 275 µs [served]
insiliscope_cli --splat-out Linear-0.5.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Linear --splat-at 0.5,0,0 --npc-dye None # 287 µs [served]
insiliscope_cli --splat-out Nearest-0.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0,0,0 --npc-dye None # 277 µs [served]
insiliscope_cli --splat-out Nearest-0.1.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0.1,0,0 --npc-dye None # 295 µs [served]
insiliscope_cli --splat-out Nearest-0.2.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0.2,0,0 --npc-dye None # 273 µs [served]
insiliscope_cli --splat-out Nearest-0.3.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0.3,0,0 --npc-dye None # 282 µs [served]
insiliscope_cli --splat-out Nearest-0.4.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0.4,0,0 --npc-dye None # 290 µs [served]
insiliscope_cli --splat-out Nearest-0.5.splat.tif --psf-kernel-half-width-nm 1500 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-interp Nearest --splat-at 0.5,0,0 --npc-dye None # 310 µs [served]
insiliscope_cli --out Fft-movie.movie.tif --x 6.58 --y -5.61 --size 16 --frames 1 --psf-interp Fft --npc-dye None # 625 ms [served]
insiliscope_cli --out Cubic-movie.movie.tif --x 6.58 --y -5.61 --size 64 --frames 20 --psf-interp Cubic --npc-dye None # 278 ms [served]
insiliscope_cli --out Linear-movie.movie.tif --x 6.58 --y -5.61 --size 64 --frames 20 --psf-interp Linear --npc-dye None # 92 ms [served]
insiliscope_cli --out Nearest-movie.movie.tif --x 6.58 --y -5.61 --size 64 --frames 20 --psf-interp Nearest --npc-dye None # 80 ms [served]
The kernel's oversampling (Renderer.PsfOversampling, cli psf-oversampling, default 6) sets how finely the kernel is sampled between camera pixel centres; its cost is the kernel's size and compute time:
| Oversampling | 2 | 4 (Fast) |
6 (Realistic) |
8 (Exhaustive) |
12 | 16 |
|---|---|---|---|---|---|---|
| Max error per pixel vs 16 (share of photons) | 0.0015 | 3.0×10-4 | 1.4×10-4 | 5.0×10-5 | 1.9×10-5 | reference |
| Default kernel: compute time | 200 ms | 940 ms | 860 ms | 3.12 s | - | - |
The kernel's samples per camera pixel (Renderer.PsfOversampling, cli psf-oversampling): one in-focus blink at (+0.37, -0.21) px, cubic placement, halo cut off, against oversampling 16. The time is the default label's default kernel (±7 µm, 71 planes) computed by --prepare 1 with nothing else running, on the build machine (4 threads).
How this figure was made (8 cli runs, 576 ms)
Generated by tools/build_physics_figures.py (figure psf-oversampling) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --splat-out os2.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 2 --splat-at 0.37,-0.21,0 --npc-dye None # 14 ms [served]
insiliscope_cli --splat-out os4.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 4 --splat-at 0.37,-0.21,0 --npc-dye None # 6.4 ms [served]
insiliscope_cli --splat-out os6.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 6 --splat-at 0.37,-0.21,0 --npc-dye None # 36 ms [served]
insiliscope_cli --splat-out os8.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 8 --splat-at 0.37,-0.21,0 --npc-dye None # 28 ms [served]
insiliscope_cli --splat-out os12.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 12 --splat-at 0.37,-0.21,0 --npc-dye None # 144 ms [served]
insiliscope_cli --splat-out os16.splat.tif --psf-kernel-half-width-nm 2000 --psf-z-range-um 0.4 --psf-halo-cut 0 --psf-oversampling 16 --splat-at 0.37,-0.21,0 --npc-dye None # 128 ms [served]
insiliscope_cli --psf-oversampling 2 --prepare 1 --npc-dye None # 220 ms
insiliscope_cli --presets-json presets.presets.json --npc-dye None # 218 µs [served]
A Cramer-Rao bound summary [ober2004] is logged to the Micro-Manager core log after each kernel computation.
Single-molecule PSFs are cut where their light is negligible (Renderer.Quality)
A blinking emitter (dSTORM, PALM, DNA-PAINT) is not drawn with its whole +/-7 µm PSF. Since 2026-10-07 each
blink's splat leaves out every camera pixel that would get less than a fixed share of the emitter's photons: the
halo cut (Renderer.PsfHaloCut, cli/viewer psf-halo-cut), set by Renderer.Quality:
| Quality | Halo cut | What it means |
|---|---|---|
| Fast | 1e-5 | no left-out pixel would have had more than 1 photon in 100 000 of the emitter's |
| Realistic (default) | 3e-6 | no left-out pixel would have had more than 3 photons in a million |
| Exhaustive | 0 | the whole kernel, every pixel (the output before 2026-10-07) |
The cut is per plane and per pixel row: it follows the PSF, a small disc in focus (~4.4 µm radius at 3e-6) and a
wide one out of focus (~8.8 µm at 3.5 µm defocus; the square's corners at 3e-6 beyond ~3 µm). Pixels inside it get
exactly the uncut values, nothing is renormalized: the left-out light (the far halo: ~0.5 % of a blink's
photons at 3e-6, in focus and averaged over the planes; 1.1 % in focus and up to 1.9 % out of focus at 1e-5;
measured 2026-10-08, the table below has the current values) is missing, not moved. On the default kernel the splat
keeps ~60 % of the square's pixels at 3e-6 averaged over the planes (29 % in focus), 40 % at 1e-5 (13 % in
focus). Measured live at 5 ms exposure (256 px, Iris Xe GPU, 2026-10-07): dSTORM ~64 -> ~91 fps
(Realistic) -> ~104 fps (Fast), DNA-PAINT ~97 -> ~100 -> ~104 fps (its frame time is mostly elsewhere). The test is strict (the largest kernel block
sum a pixel can read at any sub-pixel position, with a 1.5625x margin for cubic interpolation's overshoot; ctest
sr_render checks every left-out pixel against it).
What it does not touch: WideField labels, the mean-field and per-dye continuous populations (PALM pre states, the dSTORM initial ON phase), the free-imager background and BrightField keep the whole kernel. A dense continuous population sums the halos of very many dyes, and a cut there shows as edges of out-of-focus light. Binned blinks (approximate SMLM, dense frames) keep the kernel within the cut's largest radius over the planes, slightly more than the splat; blinks rendered mean-field keep the whole kernel.
When it matters: summing very many frames of blinks to study the PSF's far wings (localization background models,
out-of-focus haze), or comparing with webSMLM pixel for pixel. Use Exhaustive (or PsfHaloCut 0) for that.
Dim blinks are not drawn in a frame (Renderer.Quality)
Since 2026-10-14 a blink that gives a frame fewer detected photons than a minimum is left out of that frame
altogether (Renderer.BlinkMinPhotons, cli/viewer blink-min-photons), set by Renderer.Quality: Fast 100,
Realistic 50 (the default), Exhaustive 0 (every blink). The photons are the emitter's in that frame: its
brightness x the share of the frame it is ON, so the frames a blink only just starts or ends in lose it first. They
are known before the splat, so a left-out blink costs nothing. The light is missing, not moved. Such a blink would
not be localized (a few tens of photons over the background); the thresholds are an estimate, not a measured
localization limit. It applies to the splatted blinks (the CPU splat and the adapter's GPU splat); binned and
mean-field frames and every continuous population (WideField labels, pre states, the dSTORM initial ON) keep every
dye. Use Exhaustive (or BlinkMinPhotons 0) to draw every blink.
The halo cut measured on one blink and on a movie:
| Fast cut 1×10-5 |
Realistic cut 3×10-6 |
Exhaustive cut 0 |
|
|---|---|---|---|
| Blink in focus (log, 7 decades) | ![]() |
![]() |
![]() |
| Blink 2 µm out of focus | ![]() |
![]() |
![]() |
| Pixels drawn, in focus | 13 % | 29 % | 100 % |
| Photons left out, in focus | 1.1 % | 0.53 % | 0 |
| ... 2 µm out of focus | 1.7 % | 0.54 % | 0 |
| Movie: max difference (photons/px) | 0.36 | 0.11 | 0 |
| Movie: Σ|difference| / photons (%) | 0.27 | 0.088 | 0 |
| Render time per frame | 8.9 ms | 15 ms | 26 ms |
The halo cut of blinking emitters (Renderer.Quality sets Renderer.PsfHaloCut; cli psf-halo-cut): pixels that would get less than the cut's share of the photons are left out, and nothing is renormalized. Images: a 1-photon blink over the whole ±7 µm kernel square (default label), black = left out; 'pixels drawn' is their share of the square. The movie rows compare the mean photon image (before the camera) of 50 frames of the default DNA-PAINT movie (128 px) with the Exhaustive one. Times on the build machine (4 threads), averaged over the 50 frames.
How this figure was made (10 cli runs, 3.63 s)
Generated by tools/build_physics_figures.py (figure psf-halo) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --presets-json presets.presets.json --npc-dye None # 2.1 ms [served]
insiliscope_cli --splat-out Fast-z0.splat.tif --psf-halo-cut 1e-05 --splat-at 0,0,0 --npc-dye None # 439 ms [served]
insiliscope_cli --splat-out Fast-z2.splat.tif --psf-halo-cut 1e-05 --splat-at 0,0,2 --npc-dye None # 747 µs [served]
insiliscope_cli --splat-out Realistic-z0.splat.tif --psf-halo-cut 3e-06 --splat-at 0,0,0 --npc-dye None # 63 ms [served]
insiliscope_cli --splat-out Realistic-z2.splat.tif --psf-halo-cut 3e-06 --splat-at 0,0,2 --npc-dye None # 688 µs [served]
insiliscope_cli --splat-out Exhaustive-z0.splat.tif --psf-halo-cut 0 --splat-at 0,0,0 --npc-dye None # 978 µs [served]
insiliscope_cli --splat-out Exhaustive-z2.splat.tif --psf-halo-cut 0 --splat-at 0,0,2 --npc-dye None # 875 µs [served]
insiliscope_cli --photons-out Fast-movie.photons.tif --x 6.58 --y -5.61 --size 128 --frames 50 --psf-halo-cut 1e-05 --npc-dye None # 1.03 s [served]
insiliscope_cli --photons-out Realistic-movie.photons.tif --x 6.58 --y -5.61 --size 128 --frames 50 --psf-halo-cut 3e-06 --npc-dye None # 770 ms [served]
insiliscope_cli --photons-out Exhaustive-movie.photons.tif --x 6.58 --y -5.61 --size 128 --frames 50 --psf-halo-cut 0 --npc-dye None # 1.32 s [served]
Why the evaluator choice matters
A direct polar quadrature with 40 azimuthal samples aliased beyond ~1.6 µm and made every emitter 17-20% too dim on the default kernel; it was removed. Chirp-Z is the only evaluator.
PSF in the CLI and the viewer¶
The command line and the viewer use the same PSF as the adapter: GibsonLanniZernike by default (the same C++ code and
kernel cache), with options that mirror the adapter's PSF properties (Objective.*, Renderer.Psf*; psf-model, psf-zernike-preset, zern.<j>,
psf-mask, psf-oversampling, psf-kernel-half-width-nm, psf-z-range-um, psf-z-step-um, psf-sample-index,
psf-working-distance-um, psf-sample-depth-nm, psf-pupil-samples, psf-halo-cut, psf-interp; insiliscope_cli --help lists them). psf-model=0
selects the Gaussian; RichardsWolf/GibsonLanni need PSFGenerator's JVM and exist only in the adapter. The viewer uses
a 3 µm kernel half width (instead of 7 µm) to save browser memory; its blinks take the same halo cut (3e-6) within
that square.
WideField imaging¶
Dyes are binned per population into world-anchored z planes (Renderer.WideFieldZPlaneNm, default 25 nm) on a grid of
Renderer.WideFieldUpscaling (1-4) cells per pixel. Each dye plane is convolved with the PSF plane at its defocus (dye planes
go to the two neighbouring PSF planes with linear weights, i.e. a linearly z-blended PSF), by FFT, then cropped and binned to
camera pixels:
The steps on one field, slab by slab:
z-range-um) one slab apart; the four add up to the movie's mean-field image (largest difference 0 of the brightest pixel). Then the camera adds its noise.How this figure was made (6 cli runs, 2.66 s)
Generated by tools/build_physics_figures.py (figure wf-pipeline) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --photons-out h0.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mean-field-density-per-um2 0 --z 0.5 --z-range-um 1 --npc-dye None # 983 ms [served]
insiliscope_cli --photons-out h1.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mean-field-density-per-um2 0 --z 0.5 --z-range-um 3 --npc-dye None # 677 ms [served]
insiliscope_cli --photons-out h2.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mean-field-density-per-um2 0 --z 0.5 --z-range-um 5 --npc-dye None # 19 ms [served]
insiliscope_cli --photons-out h3.5.photons.tif --out h3.5.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mean-field-density-per-um2 0 --z 0.5 --z-range-um 7 --npc-dye None # 24 ms [served]
insiliscope_cli --density-out dens.density.tif --x 6.58 --y -5.61 --size 96 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mean-field-density-per-um2 0 --z 0.5 --density-z 0,4,4 --npc-dye None # 528 ms [served]
insiliscope_cli --psf-out psf.psf --mt-mode WideField --mt-dye -1 --light-preset auto --psf-kernel-half-width-nm 3000 --npc-dye None # 432 ms [served]
Implementation points that matter for accuracy and speed:
- the grid and dye tiles are anchored to the world, not the camera, so the same dyes bin the same way from every stage pose;
- the FFT size is the smallest \(2^a3^b5^c\) (multiple of 8) whose wrapped part misses the FOV;
- kernel and per-dye-plane spectra are cached, so a focus change is a re-pairing of cached spectra (about 10 ms at 256 px);
- a sub-cell stage move is a Fourier phase ramp, exact for band-limited images (pixel pitch \(\le \lambda/4\mathrm{NA}\));
- frames are weighted sums of precomputed images; bleaching uses a small basis of dose maps (or 12 Chebyshev maps) instead of per-frame spectra; in Micro-Manager's live mode the next basis of a steady view is made ahead on a background thread, so the frames do not stall when the bleaching outruns the basis in use (every frame is still exactly its own weights' image);
- coarser-grid "focus bands" exist but are gated at 0.1% rms error, which no sharp-pupil PSF currently meets.
How fine the grid and the z planes need to be:
| Mean-field grid | Difference to the reference (±9.7 photons) | rms (% of the mean) | Max / shot noise | Shift x, y vs per dye (nm) | Setup | Frame |
|---|---|---|---|---|---|---|
| Upscale 1, 25 nm planes (default) | ![]() |
1.1 | 0.60 | -8, -10 | 300 ms | 65 µs |
| Upscale 2 | ![]() |
1.2 | 0.55 | +0, -1 | 283 ms | 115 µs |
| Upscale 3 | ![]() |
0.044 | 0.020 | -8, -10 | 606 ms | 195 µs |
| 100 nm planes | ![]() |
1.2 | 0.59 | -8, -10 | 58 ms | 70 µs |
| 50 nm planes | ![]() |
1.1 | 0.59 | -8, -10 | 64 ms | 70 µs |
| 12.5 nm planes | ![]() |
1.1 | 0.60 | -8, -10 | 159 ms | 70 µs |
| Upscale 3, 12.5 nm planes (reference) | ![]() |
-8, -10 | 1.03 s | 195 µs |
The mean-field grid: cells per camera pixel (Renderer.WideFieldUpscaling, cli wf-upscale; it has to divide the PSF oversampling, so 4 becomes 3 at the default 6) and dye plane thickness (Renderer.WideFieldZPlaneNm, wf-plane-nm), against the finest grid; mEGFP WideField, 10 % labelled, 64 px, 50 ms frames, the first frame's photons before the camera. 'Max / shot noise': the largest difference over the square root of the brightest pixel (below 1 it hides in one frame's noise). The mean field puts the image off the dyes' true positions by up to 13 nm for upscale 1/3 (the shift column, fitted against the dyes rendered one by one), which also counts in the difference to the reference. Setup: the scene (the PSF kernel was loaded before); frame: the time per frame, averaged over 20 frames. Times on the build machine (4 threads).
How this figure was made (9 cli runs, 11.0 s)
Generated by tools/build_physics_figures.py (figure wf-grid) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --prepare 1 --npc-dye None # 121 ms [served]
insiliscope_cli --photons-out u1-p25.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 1 --wf-plane-nm 25 --npc-dye None # 302 ms [served]
insiliscope_cli --photons-out u2-p25.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 2 --wf-plane-nm 25 --npc-dye None # 286 ms [served]
insiliscope_cli --photons-out u3-p25.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 3 --wf-plane-nm 25 --npc-dye None # 610 ms [served]
insiliscope_cli --photons-out u1-p100.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 1 --wf-plane-nm 100 --npc-dye None # 59 ms [served]
insiliscope_cli --photons-out u1-p50.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 1 --wf-plane-nm 50 --npc-dye None # 65 ms [served]
insiliscope_cli --photons-out u1-p12.5.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 1 --wf-plane-nm 12.5 --npc-dye None # 161 ms [served]
insiliscope_cli --photons-out u3-p12.5.photons.tif --x 6.58 --y -5.61 --size 64 --frames 20 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 0 --wf-upscale 3 --wf-plane-nm 12.5 --npc-dye None # 1.04 s [served]
insiliscope_cli --photons-out perdye.photons.tif --x 6.58 --y -5.61 --size 64 --frames 1 --mt-mode WideField --mt-dye -1 --light-preset auto --mt-label-pct 10 --mean-field-density-per-um2 1000000000000 --mean-field-max-emitters 1000000000000 --npc-dye None # 8.33 s [served]
WideField through focus:
ZStage, cli z) moves from the coverslip to 4 µm above it and back, in 0.25 µm steps (every dye in the cell counts: z-range-um 0). The microtubules near the coverslip blur as the focus rises into the cell, and the higher ones (over the nucleus) sharpen. One grey scale for all frames; 9.6 µm field.How this figure was made (17 cli runs, 1.86 s)
Generated by tools/build_physics_figures.py (figure wf-defocus) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --out z0.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 0 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 1.21 s [served]
insiliscope_cli --out z0.25.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 0.25 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 501 ms [served]
insiliscope_cli --out z0.5.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 0.5 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 11 ms [served]
insiliscope_cli --out z0.75.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 0.75 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 11 ms [served]
insiliscope_cli --out z1.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 1 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z1.25.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 1.25 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z1.5.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 1.5 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z1.75.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 1.75 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z2.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 2 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z2.25.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 2.25 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z2.5.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 2.5 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z2.75.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 2.75 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 9.9 ms [served]
insiliscope_cli --out z3.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 3 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z3.25.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 3.25 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 10 ms [served]
insiliscope_cli --out z3.5.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 3.5 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 9.9 ms [served]
insiliscope_cli --out z3.75.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 3.75 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 8.2 ms [served]
insiliscope_cli --out z4.movie.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z 4 --mt-mode WideField --mt-dye -1 --light-preset auto --z-range-um 0 --npc-dye None # 8.2 ms [served]
A GPU path runs the same WGSL source on WebGPU in the viewer and on Direct3D 11 in the adapter (fp16 plane spectra, noise on the GPU), self-checks against the CPU at startup and falls back to the CPU on any failure.
BrightField imaging¶
Transmitted light through the cells (the TransmittedLamp shutter in Micro-Manager, modality=1 in the cli and viewer; CellField only). Only simulated
structures make contrast: the core's optical volume gives, per voxel, the volume fractions of cytoplasm, nucleus and
microtubule, and the renderer turns them into refractive-index slices (SampleHolder.MediumIndex 1.337,
...Cytoplasm 1.35, ...Nucleus 1.35, ...Microtubule 1.48, all estimates; optional absorption
CellField.AbsorptionPerUm). Measured at 540 nm, whole cells are 1.357-1.378 and isolated nuclei 1.343-1.353, the
nucleus below the cytoplasm, and PBS 1.335 [steelman2017]: the defaults put both
compartments within the nuclei's range.
Structures that dominate real brightfield images (nucleoli, lipid droplets, vesicles, mitochondria...) are absent until
the world simulates them.
What the BrightField renderer sees of one field, and the image it makes:
| Slice 1 of 8 | Slice 3 of 8 | Slice 6 of 8 | Slice 8 of 8 | Image, focus 1 µm below |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
What BrightField sees: the phase delay k0ΔzΔn of each 0.50 µm slice of the cells' refractive index (cytoplasm, nucleus, microtubules), from the core's optical volume, on the optical grid with its margin (one scale, at most 0.12 rad). The lamp's light (550 nm) is propagated through the slices (multislice, 24 condenser source points) and refocused; a weak phase object shows contrast mainly out of focus. Default quality 3.
How this figure was made (1 cli run, 311 ms)
Generated by tools/build_physics_figures.py (figure bf-scene) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --bf-screens-out bf.bf --photons-out bf.photons.tif --setup-json bf.setup.json --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --npc-dye None # 311 ms [served]
The model is scalar wave optics with partially coherent Koehler illumination (Abbe: the incoherent sum of the coherent
images of all source points [zuo2017]): the condenser aperture
(TransmittedLamp.CondenserNA, default 0.4, an estimate) is sampled by equal-area source points; each tilted plane
wave is propagated down through the slices (multislice [cowley1957] / beam propagation:
phase screen, then the angular-spectrum step \(e^{i k_z \Delta z}\) [zuo2017]); the exit
field is refocused to the focal plane and filtered by the objective pupil (NA and the PSF's Zernike aberrations); the
intensities of all source points add:
Since the specimen propagation does not depend on focus, a focus change costs one inverse FFT per source point.
One number trades speed for precision (Renderer.BrightFieldQuality, cli/viewer bf-quality):
| Quality | Source points | Grid cells/pixel | Slice step | 256 px setup / per focus (12 threads) |
|---|---|---|---|---|
| 1 | 6 | 1 | one thin screen | 0.19 s / 6 ms |
| 2 | 12 | 1 | 0.5 µm | 0.3 s / 10 ms |
| 3 (default) | 24 | 2 | 0.5 µm | 0.4 s / 16 ms |
| 4 | 48 | 2 | 0.25 µm | 1.5 s / 33 ms |
The levels against a finer reference:
| BrightField quality | 1 | 2 | 3 (default) | 4 | Reference |
|---|---|---|---|---|---|
| Image (1 = the lamp) | ![]() |
![]() |
![]() |
![]() |
![]() |
| Difference to the reference (±0.014 of the lamp) | ![]() |
![]() |
![]() |
![]() |
|
| rms difference (% of the contrast) | 12 | 5.9 | 3.5 | 2.0 | |
| Max difference / shot noise | 0.89 | 0.44 | 0.33 | 0.15 | |
| Setup | 157 ms | 38 ms | 70 ms | 249 ms | 1.32 s |
| Frame | < 0.1 ms | < 0.1 ms | 100 µs | 100 µs | < 0.1 ms |
Renderer.BrightFieldQuality (cli bf-quality), focal plane 1 µm below the coverslip (z = -1, where the cells show contrast), 96 px, against a reference beyond level 4 (96 source points, 4 geometry samples per cell side, 0.125 µm slices, 6 µm margin). Photons before the camera as a share of the lamp; the shot noise of one frame at the default 80000 photons/px/s and 50 ms. Times on the build machine (4 threads).
How this figure was made (5 cli runs, 1.84 s)
Generated by tools/build_physics_figures.py (figure bf-quality) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --photons-out q1.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --bf-quality 1 --npc-dye None # 159 ms [served]
insiliscope_cli --photons-out q2.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --bf-quality 2 --npc-dye None # 38 ms [served]
insiliscope_cli --photons-out q3.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --bf-quality 3 --npc-dye None # 70 ms [served]
insiliscope_cli --photons-out q4.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --bf-quality 4 --npc-dye None # 250 ms [served]
insiliscope_cli --photons-out qreference.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --z -1 --modality BrightField --bf-quality 4 --bf-sources 96 --bf-sub 4 --bf-slice-um 0.125 --bf-margin-um 6 --npc-dye None # 1.32 s [served]
Each knob can also be set alone (bf-sources, bf-upscale, bf-sub, bf-slice-um). A weak phase object in focus
shows almost no contrast; defocus brings it out with opposite signs above and below focus, as in a real microscope (the
phase transfer function is odd in defocus [zuo2017]).
Details, the package survey and the list of missing structures: spec/BRIGHTFIELD.md.
BrightField through focus:
How this figure was made (17 cli runs, 360 ms)
Generated by tools/build_physics_figures.py (figure bf-defocus) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --photons-out z-2.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -2 --npc-dye None # 297 ms [served]
insiliscope_cli --photons-out z-1.75.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -1.75 --npc-dye None # 4.1 ms [served]
insiliscope_cli --photons-out z-1.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -1.5 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z-1.25.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -1.25 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z-1.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -1 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z-0.75.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -0.75 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z-0.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -0.5 --npc-dye None # 3.7 ms [served]
insiliscope_cli --photons-out z-0.25.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z -0.25 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z0.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 0 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z0.25.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 0.25 --npc-dye None # 3.6 ms [served]
insiliscope_cli --photons-out z0.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 0.5 --npc-dye None # 3.9 ms [served]
insiliscope_cli --photons-out z0.75.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 0.75 --npc-dye None # 3.3 ms [served]
insiliscope_cli --photons-out z1.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 1 --npc-dye None # 2.8 ms [served]
insiliscope_cli --photons-out z1.25.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 1.25 --npc-dye None # 3.9 ms [served]
insiliscope_cli --photons-out z1.5.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 1.5 --npc-dye None # 5.4 ms [served]
insiliscope_cli --photons-out z1.75.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 1.75 --npc-dye None # 5.2 ms [served]
insiliscope_cli --photons-out z2.photons.tif --x 6.58 --y -5.61 --size 96 --frames 1 --modality BrightField --z 2 --npc-dye None # 5.3 ms [served]
Renderer.Quality: one knob for speed against precision¶
Micro-Manager's Renderer.Quality (Fast, Realistic (the default), Exhaustive) sets five of the knobs above together: the blink halo cut, the dimmest blink drawn, the PSF oversampling, the WideField grid and the BrightField quality; and, for fiducial beads, whether a clump of beads is drawn as one bead (Fast, Realistic) or bead by bead (Exhaustive; Fiducial beads); and, under a structured pattern, whether dSTORM's initial-ON and PALM's pre-state dyes glow under the cycle's mean light (Fast, Realistic) or each frame's pattern (Exhaustive; Structured illumination). The three levels side by side on the three kinds of image:
Renderer.Quality |
Fast bf 1, oversampling 4, wf 1, halo 1×10-5, min 100 ph |
Realistic bf 3, oversampling 6, wf 1, halo 3×10-6, min 50 ph |
Exhaustive bf 4, oversampling 8, wf 2, halo 0, min 0 ph |
|---|---|---|---|
| SMLM: mean of 30 frames | ![]() |
![]() |
![]() |
| Difference to Exhaustive (±0.86 photons) | ![]() |
![]() |
|
| rms difference (% of the range) Render time per frame |
0.22 % 4.0 ms |
0.065 % 9.1 ms |
- 17 ms |
| WideField: one frame | ![]() |
![]() |
![]() |
| Difference to Exhaustive (±76 photons) | ![]() |
![]() |
|
| rms difference (% of the range) Render time per frame |
0.81 % 70 µs |
0.59 % 70 µs |
- 120 µs |
| BrightField: focus 1 µm below | ![]() |
![]() |
![]() |
| Difference to Exhaustive (±60 photons) | ![]() |
![]() |
|
| rms difference (% of the range) Render time per frame |
12 % 30 µs |
3.7 % 30 µs |
- 30 µs |
The one Micro-Manager knob for speed against precision, Renderer.Quality, sets four engine options (the cli's preset table, Simulation/RenderPresets.h): the BrightField quality, the PSF oversampling, the mean-field grid and the blink halo cut. Photon images before the camera of the default label (SMLM), mEGFP (WideField) and the lamp (BrightField), each against Exhaustive. Times on the build machine (4 threads), per frame (averaged over 30 SMLM or 10 WideField and BrightField frames), without the setup; the PSF kernel each oversampling needs costs once per session (computed or read from the disk cache), see the oversampling table above.
How this figure was made (10 cli runs, 4.01 s)
Generated by tools/build_physics_figures.py (figure quality-bundle) from the code it documents at commit 72b1b0f, on Linux with 4 threads, 2026-11-01. Commands, in order, with their wall time (those marked [served] ran one after the other in one insiliscope_cli --serve process, which keeps the world and the PSF kernel between commands; their outputs are the same as separate runs'):
insiliscope_cli --presets-json presets.presets.json --npc-dye None # 2.1 ms [served]
insiliscope_cli --photons-out SMLM-Fast.photons.tif --x 6.58 --y -5.61 --size 96 --frames 30 --bf-quality 1 --psf-oversampling 4 --wf-upscale 1 --psf-halo-cut 1e-05 --blink-min-photons 100 --npc-dye None # 711 ms [served]
insiliscope_cli --photons-out SMLM-Realistic.photons.tif --x 6.58 --y -5.61 --size 96 --frames 30 --bf-quality 3 --psf-oversampling 6 --wf-upscale 1 --psf-halo-cut 3e-06 --blink-min-photons 50 --npc-dye None # 708 ms [served]
insiliscope_cli --photons-out SMLM-Exhaustive.photons.tif --x 6.58 --y -5.61 --size 96 --frames 30 --bf-quality 4 --psf-oversampling 8 --wf-upscale 2 --psf-halo-cut 0 --blink-min-photons 0 --npc-dye None # 1.27 s [served]
insiliscope_cli --photons-out WideField-Fast.photons.tif --x 6.58 --y -5.61 --size 64 --frames 10 --mt-mode WideField --mt-dye -1 --light-preset auto --bf-quality 1 --psf-oversampling 4 --wf-upscale 1 --psf-halo-cut 1e-05 --blink-min-photons 100 --npc-dye None # 413 ms [served]
insiliscope_cli --photons-out WideField-Realistic.photons.tif --x 6.58 --y -5.61 --size 64 --frames 10 --mt-mode WideField --mt-dye -1 --light-preset auto --bf-quality 3 --psf-oversampling 6 --wf-upscale 1 --psf-halo-cut 3e-06 --blink-min-photons 50 --npc-dye None # 178 ms [served]
insiliscope_cli --photons-out WideField-Exhaustive.photons.tif --x 6.58 --y -5.61 --size 64 --frames 10 --mt-mode WideField --mt-dye -1 --light-preset auto --bf-quality 4 --psf-oversampling 8 --wf-upscale 2 --psf-halo-cut 0 --blink-min-photons 0 --npc-dye None # 383 ms [served]
insiliscope_cli --photons-out BrightField-Fast.photons.tif --x 6.58 --y -5.61 --size 96 --frames 10 --z -1 --modality BrightField --bf-quality 1 --psf-oversampling 4 --wf-upscale 1 --psf-halo-cut 1e-05 --blink-min-photons 100 --npc-dye None # 21 ms [served]
insiliscope_cli --photons-out BrightField-Realistic.photons.tif --x 6.58 --y -5.61 --size 96 --frames 10 --z -1 --modality BrightField --bf-quality 3 --psf-oversampling 6 --wf-upscale 1 --psf-halo-cut 3e-06 --blink-min-photons 50 --npc-dye None # 70 ms [served]
insiliscope_cli --photons-out BrightField-Exhaustive.photons.tif --x 6.58 --y -5.61 --size 96 --frames 10 --z -1 --modality BrightField --bf-quality 4 --psf-oversampling 8 --wf-upscale 2 --psf-halo-cut 0 --blink-min-photons 0 --npc-dye None # 251 ms [served]




















































































































