Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ Running the tests -- two tiers:
`test_roi_session`), which is the environment and not your change.
* `SMAPPY_TEST_CAL=/path/to/_3dcal.mat` enables the tests that want real
calibration data; without it they skip.
* `SMAPPY_UIPSF_PYTHON=/path/to/python` (of an environment with uiPSF, as the
page of `Localize/uiPSF calibration` installs it) enables the slow test that
runs uiPSF itself, a few minutes on a CPU; without it it skips.

## Writing a plugin

Expand All @@ -96,6 +99,7 @@ shape, so read the one closest to what you are writing:
| finds a structure by its shape and judges it, then counts it | `npc.py` (a ring filter and circle fit in the segmenter, a per-site count, and a model of whole pores in `smappy/npc.py` fitted to the site table) |
| summarises what evaluators measured | `roi.py`'s `SiteAnalysisPlugin` (an evaluator to read, a *results from* field, a preflight that asks which when there are several) |
| runs the ROI manager's segment, evaluate and analyse as one | `npc_analysis.chain.yaml` (a shipped chain: an evaluator step runs over every ROI before the next; `docs/batch.md`, "ROIs: segment, evaluate, analyse") |
| runs an external program in its own Python | `uipsf.py` (`smappy.uipsf`: a worker script under that program's interpreter, progress read from its output, the result converted into smappy's own file) |

A chain of plugins that runs as one, and running one over many files
(`smappy-batch`, the batch window): `docs/batch.md`.
Expand Down Expand Up @@ -355,7 +359,9 @@ a new plugin there fails four tests until it is listed in both (the workspace
ones name the seeded Analysis tab, including the order its titles come back
in). `tests/test_file_plugins.py::test_the_file_tab_ships_with_the_four_it_needs`
asserts the File tab's exact contents, so a new File plugin needs a line there
too (it is a Qt test and skips without PySide6).
too (it is a Qt test and skips without PySide6). Likewise
`tests/test_workspace.py::test_the_window_opens_the_shipped_tabs` lists the
Localize tab's titles, so a new Localize plugin needs a line there.

`tests/test_control_width.py` builds every shipped plugin's panel and fails if
it is wider than the control column (`CONTROL_WIDTH`): a new plugin with a long
Expand Down
76 changes: 76 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,82 @@ Left out for now: a non-flat background, the precision from a spline PSF's
CRLB (SMAP has it) rather than Mortensen's, and using a loaded table as the
structure.

## uiPSF calibrations

`Localize/uiPSF calibration` (`smappy.uipsf`) has uiPSF learn a PSF model
from bead stacks or blinking molecules and saves it as an ordinary spline
calibration. It is a plugin of its own, not a mode of the bead calibration
window: uiPSF is optional and external, and the duplication (reading stacks,
saving) is the price of keeping it out of that window.

* **A process of its own.** uiPSF pins its own TensorFlow, so it runs under
the interpreter of its own environment (`runner.find_python`: the
plugin's setting, `uipsf_python` in the config, `$SMAPPY_UIPSF_PYTHON`,
else this Python if it has `psflearning`), as `uipsf/worker.py`, which
imports nothing of smappy's. The two talk through a job folder
(`images.npy`, `job.json`) and tagged lines on stdout; uiPSF's tqdm bars
are passed on, thinned to one per stage or five seconds. Nothing in
smappy imports TensorFlow.
* **smappy reads the data, not uiPSF.** Camera to photons, the stacks' z
from Micro-Manager, the split of a two-channel camera
(`prepare.Split`): all as smappy's own calibration and fitter do them,
and uiPSF's `prep_data` gets arrays. This is also what makes the channel
transformation recoverable: the cut is known exactly, so uiPSF's `T`
(main (y, x, 1) - imgcenter, right-multiplied, into the secondary's
flipped array) can be taken back to chip pixels and refitted as the
projective secondary -> main map `dualfit` reads.
* **Two things smappy does for uiPSF's bead finder.** It subtracts each
stack's median: uiPSF's threshold is relative to the brightest maximum
with the background in, and simulated two-colour beads (a background a
fifth of the peak) lost 15 of 18 beads to spurious maxima. And it
measures the shift between the channels by cross-correlating their
projections (`prepare.channel_shift`), which `worker.py` sets as the
multi-channel data's `shiftxy` before uiPSF pairs the beads: uiPSF's own
guess, from bead coordinates, took the neighbour on a 30 px grid of beads
and paired one bead. It is set from the worker, on uiPSF's class, rather
than by a new uiPSF parameter, so that uiPSF's main stays as it is.
* **Voxels, not uiPSF's spline.** uiPSF's `locres/coeff` is normalised to
the median plane sum after subtracting the minimum; the fitter's photons
assume the brightest plane sums to one. `res/I_model` is clipped,
normalised and splined exactly as `_single_model` /
`positive_pair_models` do (two channels jointly, the split from uiPSF's
per-channel intensities), so a fit cannot tell the calibrations apart.
* **z.** A bead model is uiPSF's `Zrange`, centred on the pupil focus, in
stack order: smappy's convention as is (checked: smappy's own calibration
of the same simulated beads has the same astigmatism against index). An
in situ model's planes are heights above the coverslip, the other way up
(uiPSF itself seeds an in situ run from a bead file's `I_model_reverse`):
reversed, with z = 0 at `stagepos * n_med / n_imm`.
* **Measured** on simulated astigmatic beads (2 stacks, 41 planes, 18 beads,
4 CPU cores, uiPSF 100 iterations): 3 minutes; Spline 3D with the result
fits simulated frames with z slope 1.03, intercept 2 nm (smappy's own
calibration: 1.00). Frames drawn with that uiPSF model and fitted with
it: slope 1.00, photons 1.00.
* **In situ is not validated.** On frames drawn with a physical (uiPSF
bead) PSF, 3000 frames, two rounds (12 minutes), the in situ model had
the right sign and zero but its z ran 0.76 of the truth, with photons 10%
high; plane by plane it matches the true PSF at about 1.3 times the
height. Started from Noll 6 instead of 5 it converged to a model whose z
ran backwards. The conversion is not the cause (a hand-written in situ
file converts exactly, tests/test_uipsf.py); whether this is uiPSF on
real data or the simulation (a demo structure only +-300 nm deep) needs
real data with a bead calibration beside it.
* **uiPSF on current numpy and TensorFlow.** uiPSF's main pinned
TensorFlow 2.9 / Python 3.7, which has no Apple-silicon build.
ries-lab/uiPSF#17 makes it run on numpy 2 and TensorFlow
2.21 (hdfdict replaced, tkinter dropped, NEP 50 dtype casts), tried with
Python 3.12 on Linux, and is meant to leave main's users where they were:
its `setup.py` still installs TensorFlow 2.9.1 / tensorflow-probability
0.17 on Python 3.7 and keeps native Windows on Python 3.8-3.10 below
TensorFlow 2.11 (the last with its GPU support) and numpy below 1.24;
only elsewhere does it allow current TensorFlow. On the old stack
(TensorFlow 2.9.1, tensorflow-probability 0.17, numpy 1.23.5; Python 3.9,
as no 3.7 could be installed here) main and #17's branch learnt the same
jobs -- one-channel beads, two-channel beads, in situ, 15 iterations each,
driven by `worker.py` -- with every array of the result bit-identical, and
`io.h5.load` (hdfdict on main, h5py on the branch) read them back alike.
The worker drives uiPSF from before #17 as well.

## Open questions

* **Pages for what is not a plugin.** Every plugin has a page behind its ?
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ where = ["src"]
# the camera database: a file that is read at run time, so it has to be in the
# wheel and not only in the checkout
# and the simulator's built-in structures, likewise
"smappy" = ["data/*.json", "data/structures/*.yaml"]
"smappy" = ["data/*.json", "data/structures/*.yaml", "data/microscopes/*.yaml"]
# the plugins' pages, read by the Help window (`smappy.docs`), one folder per
# level of the menu tree
"smappy.plugins" = ["*.chain.yaml", "docs/*.md", "docs/*/*.md", "docs/*/*/*.md", "docs/*/*/*/*.md"]
Expand Down
21 changes: 21 additions & 0 deletions src/smappy/data/microscopes/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# An example microscope profile for Localize > uiPSF calibration.
#
# Copy it to <smappy config dir>/microscopes/<name>.yaml (or to a shared folder
# named in $SMAPPY_MICROSCOPES), change the values to your instrument's, and it
# appears in the plugin's "microscope" list. The numbers here are those of
# uiPSF's own example system "M2" (config/systemtype/M2.yaml): a 1.43 NA oil
# objective with an astigmatic lens and a mirrored two-colour splitter.
name: Example (uiPSF's M2)
NA: 1.43
refractive_index: {immersion: 1.516, medium: 1.335, coverslip: 1.516}
emission_wavelength_nm: 680
# pixelsize_um: [0.127, 0.116] # x, y; leave out to take it from the camera
roi_size_px: 25
dual:
layout: up-down mirrored
main_channel: upper
split_position: null
insitu:
zernike_index: [5]
zernike_coeff: [0.5]
uipsf: {}
Loading
Loading