Skip to content

Photophysics

Every structure's label (issue 16) has a mode that says how its dyes switch: dSTORM, PALM, DNA-PAINT or WideField. The rates come from the dye's data and the light path (Dyes and light path); this page is about the switching itself. The third modality, BrightField, images transmitted light and has no photophysics: see Optics.

Each dye's schedule is a pure function of its address, so the same dye is the same dye every time the stage returns, and a window query equals the union of its slices. Time is simulated time (frame \(\times\) exposure) since the illumination came on. cli and viewer movies start at 60 s (start-sec), past the dSTORM initial ON phase; the Micro-Manager adapter reads each dye at the light its own place has had (the illumination history, below). Only blinks in the window a movie asks for are scheduled (the window \([t_0, 2t_1)\)), so a late start costs nothing extra.

What a few dyes of each mode emit over time, read from the core's schedules through the cli's dye output:

State diagrams and emission traces of single dyes per mode State diagrams and emission traces of single dyes per mode
Left: each mode's states and the rates between them, as the core schedules them (the rates below). Right: when single dyes emit, twelve dyes per mode (rows), drawn from the core's event and continuous window queries of a 1.2 µm square, with each mode's typical label and light preset. dSTORM dyes start in the initial ON phase, then blink until they bleach; PALM proteins emit green (pre state) until activated, then blink; DNA-PAINT sites blink for ever at konc; WideField dyes emit until their photon budget is spent (note the longer time axis). Dyes that blink in the window are shown first.
How this figure was made (4 cli runs, 1.38 s)

Generated by tools/build_physics_figures.py (figure photo-traces) 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 --dyes-json dSTORM.dyes.json --setup-json dSTORM.setup.json --x 6.58 --y -5.61 --mt-mode dSTORM --mt-dye -1 --light-preset auto --dyes-rect 5.98,-6.21,7.18,-5.01 --dyes-t 0,40 --npc-dye None    # 269 ms [served]
insiliscope_cli --dyes-json PALM.dyes.json --setup-json PALM.setup.json --x 6.58 --y -5.61 --mt-mode PALM --mt-dye -1 --light-preset auto --dyes-rect 5.98,-6.21,7.18,-5.01 --dyes-t 0,40 --npc-dye None    # 304 ms [served]
insiliscope_cli --dyes-json DNA-PAINT.dyes.json --setup-json DNA-PAINT.setup.json --x 6.58 --y -5.61 --mt-mode DNA-PAINT --mt-dye -1 --light-preset auto --dyes-rect 5.98,-6.21,7.18,-5.01 --dyes-t 0,40 --npc-dye None    # 406 ms [served]
insiliscope_cli --dyes-json WideField.dyes.json --setup-json WideField.setup.json --x 6.58 --y -5.61 --mt-mode WideField --mt-dye -1 --light-preset auto --dyes-rect 5.98,-6.21,7.18,-5.01 --dyes-t 0,150 --npc-dye None    # 399 ms [served]

Blinking dyes (dSTORM, PALM)

Each blink is drawn with a cut PSF

Blinks are splatted without the pixels that would get less than 3e-6 of the emitter's photons (Renderer.Quality Realistic; Fast: 1e-5; Exhaustive: the whole kernel), and a blink that gives a frame fewer than 50 detected photons (Fast: 100; Exhaustive: 0) is not drawn in that frame. Nothing is renormalized. Continuous populations keep the whole kernel and every dye. Details: Optics, the halo cut.

  1. First activation at \(t_{act} = -\ln U / k_{act}\), with \(k_{act}\) the activation rate per dark dye (from the dye's off time and the 405 nm or primed-conversion light, see the light-path page).
  2. Repeat: ON for \(\mathrm{Exp}(\tau_{on})\) (on-sec); then bleach with probability \(p_b\) (bleach-prob), otherwise dark for \(\mathrm{Exp}(\tau_{off})\) (off-sec) and blink again. For dSTORM dyes the three follow from the measured photons per switching cycle, duty cycle and switching cycles (\(\tau_{on}\) = photons / detected rate, duty cycle = \(\tau_{on}/(\tau_{on}+\tau_{off})\), \(p_b\) = 1/cycles) [dempsey2011]; the PALM values are estimates.
  3. Per-blink brightness is log-normal with mean 1 and coefficient of variation photon-cv (0 = every blink equally bright; the log-normal shape and the library's 0.5 are estimates).
  4. At most 1000 blinks per dye; \(p_b\) is clamped to [0.01, 1].

The spread of the per-blink brightness on one field:

Per-blink brightness histogram Per-blink brightness histogram
Per-blink brightness of the default label (ATTO 655 DNA-PAINT) over 30 s in a 4 µm square, from the core's event query: log-normal with mean 1 and coefficient of variation photon-cv (the line: the model's density scaled to the number of blinks); at 0 every blink is equally bright.
How this figure was made (2 cli runs, 2.59 s)

Generated by tools/build_physics_figures.py (figure photo-brightness) 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 --dyes-json cv0.5.dyes.json --setup-json cv0.5.setup.json --x 6.58 --y -5.61 --mt-dye.photon-cv 0.5 --dyes-rect 4.58,-7.61,8.58,-3.61 --dyes-t 60,90 --npc-dye None    # 1.16 s [served]
insiliscope_cli --dyes-json cv0.dyes.json --setup-json cv0.setup.json --x 6.58 --y -5.61 --mt-dye.photon-cv 0 --dyes-rect 4.58,-7.61,8.58,-3.61 --dyes-t 60,90 --npc-dye None    # 1.42 s [served]

dSTORM dyes start in an initial ON phase: every dye emits from \(t = 0\) for \(\mathrm{Exp}(\tau_{init})\) (initial-on-sec, its own draw; 2 s at the reference intensity in the library, an estimate) before the blink schedule starts. The dSTORM times scale with the excitation rate so photons per blink and duty cycle stay as measured [dempsey2011].

The initial ON phase at the start of the illumination:

t = 0 s t = 1 s t = 3 s t = 10 s t = 30 s
dSTORM photons over time from the light coming on dSTORM photons over time from the light coming on
AF647 dSTORM (3 %% labelled, its light preset) from the moment the light comes on: every dye starts in the initial ON phase (all on, the structure plain to see), then switches off and blinks. Photon images before the camera, 250 ms frames, each with its own contrast (square-root scale); the curve sums each frame's photons. cli and viewer movies start at 60 s by default (start-sec), past this phase.
How this figure was made (1 cli run, 5.84 s)

Generated by tools/build_physics_figures.py (figure photo-dstorm-start) 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 curve.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode dSTORM --mt-dye -1 --light-preset auto --start-sec 0 --exposure-ms 250 --frames 124 --npc-dye None    # 5.84 s [served]

PALM proteins with a pre state (mEos3.2 [zhang2012], Dendra2 [gurskaya2006]: green before photoconversion) emit in that state from \(t=0\) until their first activation (and bleach in it at their pre photon budget, 2e5 in the library, an estimate). The pre state has its own spectrum, so it is detected (and imaged with its own PSF) only as far as the light path lets it through.

DNA-PAINT

Persistent sites never bleach. The imager binds at rate \(k_{on} c\) [jungmann2010, schnitzbauer2017] and stays bound for \(\mathrm{Exp}(\tau_{on})\). \(k_{on}\) and \(\tau_{on}\) belong to the docking and imager sequences, so they are the target's, not the dye's (the dye sets the photons): data/specimens.json paint, options <prefix>-paint-kon and <prefix>-paint-on-sec (Micro-Manager <Target>_KonPerMolarPerSec, <Target>_BoundTimeSec, Advanced), with the imager concentration \(c\) <prefix>-imager-nm. Defaults (estimates):

Target \(k_{on}\) (M⁻¹s⁻¹) \(\tau_{on}\) mean half-time
Microtubules (mt-*) 10⁶ 0.05 s 35 ms
Nuclear pores (npc-*) 10⁶ 0.05 s 35 ms
Nuclear lamina (lamin-*) 10⁶ 0.05 s 35 ms
Nanoruler docking strands (ruler-*) 10⁶ 0.289 s 200 ms

9-10 nt imagers bind with \(k_{on}\) of this order and 9 nt docking strands for about 0.3-1 s; the microtubules' 0.05 s keeps their earlier look, and the pores and the lamina share it. The binding is a Poisson process for ever, addressed per 1 s time bin (count, start times, ON time \(\mathrm{Exp}(\tau_{on})\) capped at \(20\tau_{on}\), brightness), so any window is answered without running from \(t=0\). Overlapping binding events on one site are allowed (fine while \(k_{on}c\,\tau_{on} \ll 1\)). The free imager adds a flat background (see the light-path page; its depletion and its exclusion from cells are ignored).

WideField

Every labelled dye emits from \(t=0\) and bleaches after an emitted-photon budget \(B\) (photon-budget, per dye \(B \times\) an Exp(1) draw, i.e. a constant bleaching probability per emitted photon; the library's \(B = 10^5\) is an estimate): the bleach rate is \(\lambda = k_{em}/B\), the half time \(\ln 2\,B/k_{em}\). A frame holds the exact mean photons per dye, \(r\,(e^{-\lambda t_0} - e^{-\lambda t_1})/\lambda\) with \(r\) the detected rate, rendered mean-field or per dye.

Bleaching over 200 s, the rendered photons against the model's half time:

WideField bleaching over time WideField bleaching over time
mEGFP WideField (70 % labelled, its light preset): each dye emits until its photon budget B (per dye B x an Exp(1) draw) is spent, so the field fades exponentially with half time ln2 B/kem = 22.0 s (from the cli's setup output); the rendered frames halve at 34.7 s. Frames of 5 s.
How this figure was made (1 cli run, 1.41 s)

Generated by tools/build_physics_figures.py (figure photo-wf-bleach) 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 wf.photons.tif --setup-json wf.setup.json --x 6.58 --y -5.61 --size 48 --mt-mode WideField --mt-dye -1 --light-preset auto --start-sec 0 --exposure-ms 5000 --frames 40 --npc-dye None    # 1.41 s [served]

Continuous populations

The dSTORM initial ON, PALM pre states and WideField dyes are continuous populations: rendered mean-field while dense, per dye when sparse (details). Blinks are splatted one by one, or above 12.5 ON per µm² binned on a sub-pixel grid and FFT-convolved (approximate SMLM); a mean-field regime without blinking exists but is off by default (details).

Photons

An ON dye emits its detected rate \(\times\) brightness photons per second, integrated over the part of the frame it is on (frame-overlap weighting), times the excitation factor at its place (the beam's profile and, in HILO and TIRF, its height: Illumination geometry). In a cli or viewer movie the whole sample has been lit since \(t=0\).

Illumination history (Micro-Manager adapter)

The adapter remembers what light every place of the sample has had. The sample's time is cut into records of one light (its kind, its power, where the beam sat), and each record is added, at the beam's exact place, to maps of the dose per place. Every dye is read at the dose at its own place and height, smooth in all three directions: the bleached area has the beam's soft edge, after TIRF the dyes higher in a cell are still fresh, and a HILO sheet bleaches its own inclined slab. This holds for every mode and however the dyes are drawn (blinks, a mean field, dye by dye). It replaced (2026-10-16) a history that counted the beam in two levels per 0.25 µm tile and 12 height bands, with which blinking and sparse populations stepped at the beam's half contour. Each kind of light keeps the light path and label settings in force then (the laser lines' power ratios, excitation filter, dichroic, imager concentration, the dye's kinetic overrides), and every dye's schedule is read through that rate history with the same random draws whatever the history, so a change acts from now on: the past stays as it was and the future follows the new rates.

  • Bleach a region, move away and come back: it is still dim. A place never lit is fresh (dSTORM dyes in their initial ON phase, PALM proteins unconverted, WideField dyes unbleached).
  • PALM, 405 nm off: the proteins converted so far blink on until they bleach, no new ones follow; 405 nm up: the density of blinking dyes rises over the activation time instead of jumping.
  • More (or less) laser power in dSTORM or WideField: the field continues from its present state at the new rates (WideField x4 power: the next frame is up to 4x as bright, a little less as the dyes bleach 4x faster within it; the dyes bleached so far stay bleached).
  • DNA-PAINT: a change of imager concentration acts at once (the binding events are drawn per second; a second with a change takes each part at its own rate); the bindings before the change never change.
  • Another dye or mode for a structure: the past light is re-read for the new dye (the new dye as it would be under the light the place has had).

What needs no light runs on the sample's time, lit or not: DNA-PAINT binding and PALM's spontaneous activation. What the light drives (dSTORM switching, WideField and pre-state bleaching, PALM's 405 nm activation) advances with the dose: a dye no light reaches now is frozen and emits nothing, and the beam's dim rim is lit and bleaches at its own, slower rate.

Two of these changes, rendered by the cli with the same rate history (--history-before):

A rate change acts from now on A rate change acts from now on
The rate history (Micro-Manager's illumination history), reproduced with the cli's --history-before: the sample has been lit since time 0, the light changes at the grey line. Left: mEos3.2 PALM, 405 nm switched off at 20 s (0.0020 kW/cm2 to 0): the proteins converted so far blink out within seconds, no new ones follow. Right: mEGFP WideField, 488 nm from 0.010 to 0.040 kW/cm2 at 30 s: the next frame is about 4x as bright and the field bleaches 4x as fast from what is left. Dashed: the new setting applied to the whole past instead, as if it had been on from the start. Photons before the camera, 64 px, frames of 0.5 s (PALM) and 1 s (WideField).
How this figure was made (8 cli runs, 3.44 s)

Generated by tools/build_physics_figures.py (figure photo-history) 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 --setup-json PALM-setup.setup.json --x 6.58 --y -5.61 --size 64 --mt-mode PALM --mt-dye -1 --light-preset auto --exposure-ms 500 --npc-dye None    # 9.2 ms [served]
insiliscope_cli --photons-out PALM-unchanged.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode PALM --mt-dye -1 --light-preset auto --exposure-ms 500 --start-sec 0 --frames 80 --npc-dye None    # 1.92 s [served]
insiliscope_cli --photons-out PALM-history.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode PALM --mt-dye -1 --light-preset auto --exposure-ms 500 --start-sec 20 --frames 40 --laser-405 0 --history-before laser-405=0.002 --npc-dye None    # 48 ms [served]
insiliscope_cli --photons-out PALM-reread.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode PALM --mt-dye -1 --light-preset auto --exposure-ms 500 --start-sec 20 --frames 40 --laser-405 0 --npc-dye None    # 15 ms [served]
insiliscope_cli --setup-json WideField-setup.setup.json --x 6.58 --y -5.61 --size 64 --mt-mode WideField --mt-dye -1 --light-preset auto --exposure-ms 1000 --npc-dye None    # 6.2 ms [served]
insiliscope_cli --photons-out WideField-unchanged.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode WideField --mt-dye -1 --light-preset auto --exposure-ms 1000 --start-sec 0 --frames 100 --npc-dye None    # 677 ms [served]
insiliscope_cli --photons-out WideField-history.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode WideField --mt-dye -1 --light-preset auto --exposure-ms 1000 --start-sec 30 --frames 70 --laser-488 0.04 --history-before laser-488=0.01 --npc-dye None    # 382 ms [served]
insiliscope_cli --photons-out WideField-reread.photons.tif --x 6.58 --y -5.61 --size 64 --mt-mode WideField --mt-dye -1 --light-preset auto --exposure-ms 1000 --start-sec 30 --frames 70 --laser-488 0.04 --npc-dye None    # 383 ms [served]

A frame counts its light (the whole beam, out to where it falls below 10-4 of its peak) for one exposure when the lasers' shutter is open, the light path carries power to the sample, and the frame is acquired (a snap or a sequence acquisition) or, with SampleHolder.TimeWhileIdle = Running (the default), the shutter was opened by hand (Live off with the shutter open still lights the sample, as on a real microscope; with autoshutter, the shutter is closed between acquisitions). Paused stops the sample's time between acquisitions: no light, no drift, no binding. The sample's time starts with its first light (before it nothing has happened to the sample). A snap takes a frame started after it was called, a sequence acquisition frames started after it began, so the first frame after a stage move shows the new place. The lit area follows the sample drift (Camera), and the background fade reads the dose at the FOV centre. A stack reads the history once at its first frame and adds its frames, as one record of the light along its own seeded drift path (so a stack is reproducible only on a freshly loaded device). The history is cleared when the world changes (seed, cell parameters). Precision: the dose is bilinear between nodes 0.25 µm apart (closer under a narrow beam, so it is within 0.1 % of the peak), its height dependence in 5 nm steps; a record keeps the beam's place to 25 nm.