Skip to content

Add uiPSF calibration plugin for learning PSF models - #5

Merged
jries merged 7 commits into
mainfrom
claude/friendly-cerf-qt2sfj
Oct 7, 2026
Merged

jries merged 7 commits into
mainfrom
claude/friendly-cerf-qt2sfj

Conversation

@jries

@jries jries commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Localize/uiPSF calibration, a plugin that uses uiPSF (Liu et al. 2024) to learn a PSF model. It learns from bead z-stacks or from the blinking molecules of an acquisition (in situ), in one channel or two (split camera). The result is saved as an ordinary smappy calibration file, which Spline 3D and Spline 3D 2C read unchanged; uiPSF's own result is saved beside it.

uiPSF is optional and not installed by smappy. It runs in a Python environment of its own; the plugin's page explains how to set one up.

Merge ries-lab/uiPSF#17 first. The page's install instructions clone uiPSF's main, and only with #17 merged does main install on Python 3.12 / current TensorFlow (numpy 2, macOS). An existing environment made from uiPSF's main before #17 also works.

Changes

  • smappy.uipsf

    • prepare: reads the data and converts it to photons. Cuts a split camera into its channels, recording exactly how. Subtracts each bead stack's median, because uiPSF's bead threshold includes the background. Measures the channel shift by cross-correlation, because uiPSF's own guess pairs neighbours on a regular bead grid.
    • microscopes: YAML microscope profiles (NA, refractive indices, wavelength, splitter, starting pupil, any uiPSF parameter). They are read from $SMAPPY_MICROSCOPES, <config dir>/microscopes, then the shipped example.
    • uipsf_parameters: the profile and the settings as uiPSF's configuration.
    • runner + worker.py: uiPSF runs as a subprocess under its own interpreter. The worker imports nothing of smappy's. They talk through a job folder and tagged stdout, and the progress bars are passed on. The worker also sets the measured channel shift on uiPSF's class, so uiPSF needs no new parameter.
    • convert: uiPSF's voxel model is clipped, normalised and splined as smappy's bead calibration does it. Two channels are normalised jointly. uiPSF's T is refitted as smappy's secondary → main projective map in chip pixels. In situ models are reversed and zeroed at the nominal focal plane.
    • plots: uiPSF's notebook diagnostics as result tabs: data vs model, localization bias, pupil, a Zernike bar chart, beads/emitters used, and the channel transformation's residuals.
  • Plugin (plugins/uipsf.py) and its page, which includes installing uiPSF.

  • tests/test_uipsf.py: conventions checked on hand-written uiPSF files:

    • z sign and zero for bead and in situ models;
    • the split geometry for all four layouts;
    • T → transformation;
    • joint normalisation;
    • the channel shift on a bead grid;
    • every result tab drawn as the window draws it;
    • the runner protocol.

    With SMAPPY_UIPSF_PYTHON set, one test also runs uiPSF itself and fits the result back with Spline 3D.

  • test_workspace lists the new Localize plugin. CLAUDE.md gains a plugin-shape row and two test notes. NOTES.md has a "uiPSF calibrations" section with the decisions and measurements. The example profile ships in data/microscopes.

Results on simulated data (4 CPU cores, TensorFlow 2.21)

time result
beads, one channel ~3 min Spline 3D fit: z slope 1.03, intercept 2 nm
beads, two channels ~10 min transformation within 0.02 px of the true one; photon split 0.499/0.501 (true 0.5); Spline 3D 2C: z slope 1.015, colour ratio exact
blinking, one channel ~12 min right z sign and zero, but z scale 0.76 and photons +10%; sensitive to the starting pupil

On the old stack (TensorFlow 2.9.1, numpy 1.23.5), the worker drove uiPSF's main before #17 through all three kinds of job, with results bit-identical to #17's branch.

In situ is not validated. The page says to check an in situ model against a bead calibration of the same microscope. Not tried end to end: a mirrored splitter with uiPSF (the geometry is unit-tested), two-channel in situ, real data, Windows, and macOS.

--slow -n auto: 1346 passed, 4 skipped (with SMAPPY_UIPSF_PYTHON set, so the uiPSF test ran). The last commit changes only the page's install line and NOTES.md; the page and uiPSF tests pass on it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV

claude added 5 commits October 6, 2026 10:01
…ine 3D

A plugin of its own (not a mode of the bead calibration window) that has
uiPSF learn a PSF from bead z-stacks or from the blinking molecules of an
acquisition (in situ), in one channel or two, and saves it as smappy's
calibration file, which Spline 3D and Spline 3D 2C read unchanged.

- smappy.uipsf: prepare (camera to photons, the split of a two-channel
  camera recorded so uiPSF's channel transformation can be taken back to
  chip pixels, background subtraction and the channel shift for uiPSF's
  bead finder), microscopes (YAML profiles: NA, refractive indices,
  wavelength, splitter; $SMAPPY_MICROSCOPES, <config>/microscopes, shipped
  example), runner + worker (uiPSF runs in its own Python, as a script
  importing nothing of smappy's; progress read from its output), convert
  (uiPSF's voxels splined and normalised as smappy's bead calibration does;
  uiPSF's T refitted as the secondary -> main projective map)
- the plugin's page, and NOTES.md "uiPSF calibrations" with what was
  measured
- tests/test_uipsf.py: conventions on hand-written uiPSF files (z sign and
  zero for bead and in situ models, split geometry for all four layouts,
  T -> transformation, joint normalisation), profiles, parameters, the
  runner protocol; and, when SMAPPY_UIPSF_PYTHON is set, uiPSF itself on
  simulated beads fitted back by Spline 3D (slow)

Needs the claude/friendly-cerf-qt2sfj branch of ries-lab/uiPSF (numpy 2,
current TensorFlow, channel_shift).  On simulated data: single-channel
beads fit with z slope 1.03, dual-channel beads with a transformation
0.02 px off and z slope 1.015.  In situ learning runs but its z scale came
out 0.76 on simulated data; the page says to check it against beads.

--slow -n auto: 1342 passed, 4 skipped (with SMAPPY_UIPSF_PYTHON set)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
…long

- the plugin's page: "Installing uiPSF", after "What it does" -- smappy does
  not install it; a conda environment with uiPSF's branch, a check, and the
  three ways to tell smappy where it is (config.yaml per platform, the
  plugin's setting, SMAPPY_UIPSF_PYTHON), and what a graphics card needs.
  Checked in a fresh Python 3.12 venv: pip install -e of the branch, then
  the real-uiPSF test passes.  The page's code blocks fenced, which is what
  the page renderer turns into code (indented ones became paragraphs)
- CLAUDE.md: the plugin-shape row for a plugin that runs an external
  program in its own Python; the Localize tab's titles listed in
  test_workspace; SMAPPY_UIPSF_PYTHON enabling the uiPSF test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
uiPSF draws its checks in its notebooks (psflearning.makeplots), which make
their own figures and call plt.show(); smappy.uipsf.plots redraws them from
the saved result into the figure a result window hands over, one tab each:

- data vs model (showpsfvsdata / _insitu): a typical bead, or molecules
  averaged per model plane by fitted height, above the model, with x-z
- localization bias (showlocalization): every bead refitted plane by plane,
  x, y, z bias in nm against stage z, the median in red; beads only
- pupil (showzernike): magnitude, aberration phase from Noll 5 up in nm,
  and every Zernike term
- beads / emitters (showcoord): found and used
- channel transformation (showtransform): the pairs' residuals after T, in
  nm, as arrows over the field and scattered; two channels only

They replace the plugin's own model and Zernike figures.  The text adds the
beads' median bias and the transformation's rms residual.  The page's
Output section describes each tab and what a good one looks like.

--slow -n auto: 1346 passed, 4 skipped (with SMAPPY_UIPSF_PYTHON set)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
- a "Zernike" tab after "pupil": the aberration phases from Noll 5 up in nm
  rms as bars, the channels side by side, the named terms labelled
- the beads/emitters tab of a one-channel result failed to draw in the
  window: one panel is handed an axis, not a figure.  The tests now draw
  every tab through Plot.draw_into, as the window does, which is what
  found it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
uiPSF's main stays without a channel_shift parameter: the worker sets the
measured shift (prepare.channel_shift) as the multi-channel data's shiftxy
before uiPSF pairs the beads, on uiPSF's class, since prep_data makes and
processes the object in one call.  The shift travels in job.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
claude added 2 commits October 6, 2026 20:42
- NOTES.md, "uiPSF's branch": the branch's setup.py keeps main's
  TensorFlow where it was pinned; on the old stack (TensorFlow 2.9.1,
  numpy 1.23.5) main and the branch learnt the same jobs bit-identically,
  and the worker drives an unchanged main as well
- the page: an environment already made from uiPSF's main works too

--slow -n auto: 1346 passed, 4 skipped (with SMAPPY_UIPSF_PYTHON set)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
The page's install line clones uiPSF's main rather than the branch of
ries-lab/uiPSF#17, which is to be merged first; NOTES.md speaks of #17
rather than of the branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiNdULxyiqQEqWHGKTbVzV
@jries
jries merged commit dfe02b2 into main Oct 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants