Cluster workflow (Snakemake + SLURM)
The workflow/ directory runs patchworks on a SLURM cluster: the image is
converted once, its tiles are segmented as GPU jobs in parallel, and the
results are merged into the store, with an object table per label image.
Several segmentations of one image can run together and be related to each
other (which cell each nucleus is in).
Prefer a form to YAML?
The web launcher builds the config, submits the run and follows it from a browser.
1. Set up
Keep workflow/ on a filesystem the compute nodes can read: jobs run the
environment's Python directly. The environments use this clone's own
patchworks, so git pull is all an update takes.
Optional environments add methods; every task works in them (pixi run -e
<env> ...):
| Environment | Adds |
|---|---|
cellpose3, cellpose4 |
a pinned Cellpose version |
cuda12, cuda13 |
cupy, for GPU DoG and dilate_gpu (match nvidia-smi) |
plantseg |
PlantSeg and cupy (membrane cells) |
careamics |
Noise2Void denoising (denoise:) |
viewer |
napari, for looking at results |
A CUDA environment cannot be installed or run on a login node without a
GPU; tell pixi a driver is there: export CONDA_OVERRIDE_CUDA=12.0.
2. Configure
Copy config/config.yaml into config/local/, which git ignores, and edit
the copy: the shipped files then stay untouched, so git pull never
conflicts with your settings. Add --configfile to the commands below; it
replaces the shipped config rather than adding to it:
mkdir -p config/local && cp config/config.yaml config/local/my_run.yaml
pixi run dry --configfile config/local/my_run.yaml
input: "/data/scan.ims" # .ims/.czi/.lif/.nd2/OME-TIFF/.zarr
work_dir: "/scratch/results" # everything is written here
label_name: "cells" # becomes image.zarr/labels/cells
channel: 0
overlap: [4, 30, 30] # halo: about one object diameter per axis
cellpose:
model: "cyto3"
diameter: 30
do_3D: true
Only input and work_dir are required. Every other setting is listed in
the file, commented out with its default and a one-line description: remove
the # to change one. In a multi run, settings shared by every
segmentation are listed in common.yaml, the rest in each segmentation's
config. The options most worth knowing are below. Then edit
profile/slurm/config.yaml for your cluster: partitions, the GPU request,
jobs: (how many jobs at once). It ships with sciCORE's values; for
another cluster, copy the folder to profile/<cluster>/ and pass
--workflow-profile profile/<cluster> (--profile to run_multi), or name
it as the preset's profile: in the launcher. The segment jobs' memory is
set once, under prepare:, and used by both: prepare sizes
tile_shape: "auto" tiles to it.
runtime is capped by the QOS
A job asking for more time than its QOS allows is refused by sbatch
and never starts; its log stays empty. Ask for a QOS that allows it:
qos: "1day" next to the runtime:. sacctmgr show qos
format=name,maxwall lists the ceilings.
3. Run
C=config/local/my_run.yaml
pixi run check $C # the config's problems; once converted, the plan
pixi run fetch $C # its models, here (GPU nodes are often offline)
pixi run channels $C # once converted: a picture per channel
pixi run dry --configfile $C # checks the config, lists the steps; runs nothing
pixi run go --configfile $C # locally, on this machine
pixi run slurm --configfile $C # on the cluster: submits and follows the jobs
pixi run status $C # how far it got (from another shell)
The dry run checks the whole config first (unknown keys, wrong values,
packages the environment lacks), so a mistake shows in seconds rather
than after the conversion. Run slurm in tmux on the login node: it
follows the jobs until the end. A run that stops (or fails) resumes where
it was when started again with the same command; finished steps are not
redone. Once the image is converted, check also plans each
segmentation with the run's own tile sizing: tiles, how many hold signal,
GPU jobs, memory per tile, size of the result. check $C --sample 3 times
the method on 3 real tiles for the hours a job and the run will take, and
--preview DIR adds a picture of one of them per segmentation, its
objects outlined, to judge the settings before the run: run it where the
jobs run (srun with the segment job's partition and GPU).
status reads what the run left in work_dir: converted or not,
tiles segmented out of how many, merged with how many objects, the seam
report, and for a multi config its relations and bundle. A step that
failed is named with the line of its log that says why and, for the
usual causes (GPU or RAM out of memory, a time limit, a missing package,
a download on an offline node, a full disk, a stale lock), what to do.
Several segmentations and their relations
config/multi.yaml lists segmentation configs and which label images to
relate, and runs them all with one command:
common: config/common.yaml # shared: input, work_dir, tile_shape, ...
segmentations:
- config/config_nuclei.yaml # each holds only what differs
- config/config_cyto.yaml
- config/config_cilia.yaml
relations:
- {a: nuclei_labels, b: cyto_labels, output: nuclei_to_cyto.xlsx}
- {a: cilia_labels, b: cyto_labels, output: cilia_to_cyto.xlsx, max_distance_um: 1.0}
review: # optional: what `patchworks review` flags
expect: {cyto_labels: {nuclei_labels: 1}}
# bundle: false # the store is zipped at the end unless told not to
What it does:
- Checks everything first: the configs agree on
work_dir,leveland conversion settings, label names are unique, relations name label images that exist, each environment has what its method needs. - Converts once, then runs the segmentations concurrently. A config
with
seed_labels:(cells grown from nuclei) waits for the one it grows from. Each segmentation's Snakemake output also goes to<work_dir>/<label_name>/snakemake.out, and how it ended torun_multi_state.jsonbeside it, whichpixi run statusand the launcher show. - Relates each pair: one job per parent label image, reading it once
for all its children. Each relation gives a workbook with one row per
child (its parent, the overlap) and one per parent (how many children); a
sheet too long for Excel is written as
.csv.max_distance_umgives a child touching no parent the nearest one within that distance. - Bundles the store into one
image.zarr.zipwhen everything succeeded (bundle: falseskips it;bundle: {format: iso}makes a disk image instead).
Re-running the same command redoes only what is missing or out of date.
Only one run can drive a work_dir at a time. A relative --config is
found from where you run pixi; the paths inside it, next to it.
Relate jobs take relate: {cpus: 16, mem: "32G", time: 180, qos: "1day"}
in the multi config (the work is mostly reading; more CPUs help up to the
filesystem's speed).
Outputs
work_dir/
image.zarr/ the image, as a pyramidal OME-Zarr
image.zarr/labels/<name>/ each label image (pyramid, calibrated)
image.zarr/labels/<name>/table one row per object: size, position, shape, relations
<relation>.xlsx one workbook per relation
image.zarr.zip the whole store in one file (multi runs)
<name>/logs/ one log per step and per tile batch
Look at the results with pixi run -e viewer napari work_dir/image.zarr,
and check them with patchworks review (Reviewing). The store
opens in any OME-Zarr reader; a store from an older version can be brought
up to the spec with patchworks fix-metadata image.zarr.
Options worth knowing
Two channels for Cellpose — nuclei_channel: 1 gives Cellpose 3 the
nuclear stain as a second input. Cellpose 4 (cpsam) next to a bright
nuclear channel may segment the nuclei instead of the cells: give it the
membrane alone, or grow cells from the nuclei (membrane
cells).
Cellpose model names differ between versions: v3 has cyto3,
nuclei; v4 the cpsam family. A name the installed version lacks is
refused before anything runs. For 3-D, the anisotropy is taken from the
image's calibration; set cellpose: {anisotropy: 1} if results show rings
along z (faster, slightly less exact at the top and bottom).
Same contrast in every tile — Cellpose rescales each input from its
own percentiles, which gives every tile its own contrast and cuts cells at
the seams. The workflow measures the image's 1–99% range once, from
full-resolution samples of the tiles to segment, and scales every tile with
it. normalize: "tile" restores Cellpose's per-tile behaviour. The same
goes for PlantSeg, which standardizes with the image's mean and standard
deviation, and for the membrane plugins' foreground: "otsu" and nuclei
threshold: one Otsu threshold for the whole image, not one per tile. The values used
are in the labels' provenance.
Fragments in 3-D — 3-D Cellpose often splits a cell into a body and
slivers, and leaves specks in dim regions. With do_3D, each tile's
objects smaller than a tenth of a typical cell are merged into the cell they
touch most (or dropped when they touch nothing), and the flows are smoothed
(flow3D_smooth: 1). Change the share with cellpose: {fragments: 0.05},
or turn it off with fragments: null.
Halo — overlap: "auto" sizes it from the objects: an earlier run's
table, or the tiles tried in the launcher, else the Cellpose diameter
(z shortened by the anisotropy). A halo of about one object is what lets
both tiles see a cell crossing their seam whole.
Size filter — min_volume / max_volume (µm³) drop objects outside
that range after the merge, judged on whole objects, not tile fragments.
Connectivity — connectivity: 3 (threshold method, DoG plugin) joins
voxels touching at edges and corners too: thin oblique structures such as
cilia stay whole. Tiles are joined the same way.
Growing labels — dilate: N grows every label by N pixels;
dilate_gpu: true does it on the GPU (needs a CUDA environment).
Batching — tiles_per_job: 4 segments four tiles per job with one
model load. Keep the batch's run time under the job's runtime.
Empty tiles — skip_empty: true (default) skips background tiles,
found from a low-resolution occupancy map. If tiles inside the tissue come
out empty (seams.json lists cells ending against nothing), lower
empty_threshold: or set skip_empty: false.
Fewer files — shard: true packs chunks into shards: far fewer files
on a filesystem that dislikes many small ones. shard_labels: true also
packs label level 0, one tile per shard, as the merge writes it (no extra
pass); a list (shard_labels: [126, 2048, 2048]) sets another shard shape,
at the cost of a second pass over level 0. Choose before
converting, or repack a finished store with pixi run reshard --store
image.zarr.
OME-Zarr version — ngff_version: "auto" writes 0.5 (zarr v3). Set
"0.4" (zarr v2, no sharding) for a reader that cannot read 0.5 yet.
One file to copy — a multi run ends with image.zarr.zip; for a single
run, pixi run zip --store image.zarr. zarr, napari and patchworks read
the store straight out of the zip. pixi run iso makes a read-only disk image instead, if
xorriso, genisoimage or mkisofs is installed.
Email — notify_email: "you@example.org" mails you when the long steps
finish or fail, with the failing step's log. Test it with python
scripts/run_multi.py --config my_multi.yaml --test-email.
Your own segmentation function
method: "custom" imports any function from a tile to labels:
method: "custom"
custom:
module: "my_seg" # a file in workflow/scripts/, or an installed package
function: "segment"
kwargs: {threshold: 0.5}
The segment jobs' environment needs everything it imports. GPU nodes are often offline: download model weights once on the login node. See Custom segmentation function.
Troubleshooting
| Symptom | Fix |
|---|---|
Directory cannot be locked |
a run was killed: add --unlock to the same command, then run again |
another run_multi ... is already driving |
an earlier run is still going: wait, or stop it (the message names its pid) |
| A job failed, its SLURM log is empty | read the step's own log under work_dir/<label_name>/logs/ |
| A step looks hung | every step logs progress about once a minute: tail -f its log |
| Jobs pend forever | wrong partition or GPU request in the profile |
Network is unreachable in a segment job |
offline GPU node: fetch the model once on the login node |
Virtual package '__cuda' does not match |
export CONDA_OVERRIDE_CUDA=12.0 on the login node |
| Out of GPU memory | smaller tile_shape, or do_3D: false |
No module named ... in a job |
the method needs another environment (-e plantseg, -e cuda12, ...); the run says which |