patchworks 0.11.6__tar.gz → 0.11.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {patchworks-0.11.6 → patchworks-0.11.8}/.gitignore +2 -0
  2. {patchworks-0.11.6 → patchworks-0.11.8}/PKG-INFO +5 -3
  3. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/snakemake.md +143 -1
  4. {patchworks-0.11.6 → patchworks-0.11.8}/pyproject.toml +5 -1
  5. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/Snakefile +5 -0
  6. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/config/config.yaml +11 -4
  7. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/pixi.toml +1 -1
  8. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/profile/slurm/config.yaml +6 -3
  9. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/rules/common.smk +4 -0
  10. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/rules/segment.smk +16 -0
  11. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/scripts/_pw.py +19 -2
  12. patchworks-0.11.8/workflow/scripts/fetch_model.py +21 -0
  13. patchworks-0.11.6/workflow/.snakemake/iocache/latest.pkl +0 -0
  14. {patchworks-0.11.6 → patchworks-0.11.8}/.github/workflows/docs.yml +0 -0
  15. {patchworks-0.11.6 → patchworks-0.11.8}/.github/workflows/lint.yml +0 -0
  16. {patchworks-0.11.6 → patchworks-0.11.8}/.github/workflows/release.yml +0 -0
  17. {patchworks-0.11.6 → patchworks-0.11.8}/.markdownlint-cli2.yaml +0 -0
  18. {patchworks-0.11.6 → patchworks-0.11.8}/README.md +0 -0
  19. {patchworks-0.11.6 → patchworks-0.11.8}/cliff.toml +0 -0
  20. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/chunks.md +0 -0
  21. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/cluster.md +0 -0
  22. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/io.md +0 -0
  23. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/merge_tile_labels.md +0 -0
  24. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/plugins/cellpose.md +0 -0
  25. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/plugins/napari.md +0 -0
  26. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/plugins/ome_zarr.md +0 -0
  27. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/relabel.md +0 -0
  28. {patchworks-0.11.6 → patchworks-0.11.8}/docs/api/tile_process.md +0 -0
  29. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/cellpose_2d.md +0 -0
  30. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/cellpose_2d.py +0 -0
  31. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/cellpose_3d.md +0 -0
  32. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/cellpose_3d.py +0 -0
  33. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/custom.md +0 -0
  34. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/custom_method.py +0 -0
  35. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/standalone_merge.md +0 -0
  36. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/stardist.md +0 -0
  37. {patchworks-0.11.6 → patchworks-0.11.8}/docs/examples/stardist_2d.py +0 -0
  38. {patchworks-0.11.6 → patchworks-0.11.8}/docs/getting_started.md +0 -0
  39. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/gpu_distributed.md +0 -0
  40. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/merging.md +0 -0
  41. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/ome_zarr_napari.md +0 -0
  42. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/performance.md +0 -0
  43. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/pitfalls.md +0 -0
  44. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/skip_empty.md +0 -0
  45. {patchworks-0.11.6 → patchworks-0.11.8}/docs/guide/tiling.md +0 -0
  46. {patchworks-0.11.6 → patchworks-0.11.8}/docs/index.md +0 -0
  47. {patchworks-0.11.6 → patchworks-0.11.8}/mkdocs.yml +0 -0
  48. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/__init__.py +0 -0
  49. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_chunks.py +0 -0
  50. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_cluster.py +0 -0
  51. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_core.py +0 -0
  52. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_distributed.py +0 -0
  53. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_io.py +0 -0
  54. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_merge.py +0 -0
  55. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/_relabel.py +0 -0
  56. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/plugins/__init__.py +0 -0
  57. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/plugins/cellpose.py +0 -0
  58. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/plugins/napari.py +0 -0
  59. {patchworks-0.11.6 → patchworks-0.11.8}/src/patchworks/plugins/ome_zarr.py +0 -0
  60. {patchworks-0.11.6 → patchworks-0.11.8}/tests/test_core.py +0 -0
  61. {patchworks-0.11.6 → patchworks-0.11.8}/tests/test_distributed.py +0 -0
  62. {patchworks-0.11.6 → patchworks-0.11.8}/tests/test_napari.py +0 -0
  63. {patchworks-0.11.6 → patchworks-0.11.8}/tests/test_ome_zarr.py +0 -0
  64. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/README.md +0 -0
  65. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/rules/convert.smk +0 -0
  66. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/rules/merge.smk +0 -0
  67. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/scripts/convert.py +0 -0
  68. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/scripts/merge.py +0 -0
  69. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/scripts/prepare_tiles.py +0 -0
  70. {patchworks-0.11.6 → patchworks-0.11.8}/workflow/scripts/segment_tile.py +0 -0
@@ -7,3 +7,5 @@ build/
7
7
  site/
8
8
  .pytest_cache/
9
9
  *.log
10
+ .snakemake/
11
+ .pixi/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 0.11.6
3
+ Version: 0.11.8
4
4
  Summary: Tiled processing of arbitrarily large images with globally consistent labels
5
5
  Project-URL: Homepage, https://github.com/imcf/patchworks
6
6
  Project-URL: Issues, https://github.com/imcf/patchworks/issues
@@ -30,7 +30,8 @@ Requires-Dist: bioio-nd2; extra == 'all'
30
30
  Requires-Dist: bioio-ome-tiff; extra == 'all'
31
31
  Requires-Dist: bioio-tifffile; extra == 'all'
32
32
  Requires-Dist: imaris-ims-file-reader; extra == 'all'
33
- Requires-Dist: napari[all]; extra == 'all'
33
+ Requires-Dist: lxml-html-clean; extra == 'all'
34
+ Requires-Dist: napari[all]>=0.5.5; extra == 'all'
34
35
  Requires-Dist: nvidia-ml-py; extra == 'all'
35
36
  Requires-Dist: psutil; extra == 'all'
36
37
  Requires-Dist: scikit-image; extra == 'all'
@@ -62,7 +63,8 @@ Provides-Extra: io
62
63
  Requires-Dist: psutil; extra == 'io'
63
64
  Requires-Dist: tqdm; extra == 'io'
64
65
  Provides-Extra: napari
65
- Requires-Dist: napari[all]; extra == 'napari'
66
+ Requires-Dist: lxml-html-clean; extra == 'napari'
67
+ Requires-Dist: napari[all]>=0.5.5; extra == 'napari'
66
68
  Provides-Extra: workflow
67
69
  Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
68
70
  Requires-Dist: snakemake>=8; extra == 'workflow'
@@ -194,6 +194,146 @@ also re-runs a step when its **code, params or software environment** change —
194
194
  so upgrading patchworks would re-do the conversion and overwrite an existing
195
195
  result. Keep `mtime` and reruns happen only when an output is missing or stale.
196
196
 
197
+ ## Custom segmentation function
198
+
199
+ Not using Cellpose? Run **your own** per-tile function — no need to edit the
200
+ package. You write one function; patchworks handles everything around it
201
+ (tiling, halos, skipping empty tiles, the zarr-native merge, global relabelling,
202
+ resume, logs).
203
+
204
+ ### The contract
205
+
206
+ Your function is called **once per tile**:
207
+
208
+ ```python
209
+ labels = segment(tile) # plus any kwargs you configure
210
+ ```
211
+
212
+ | | What you get / must return |
213
+ | --- | --- |
214
+ | **Input `tile`** | A NumPy array of **one** tile, with the overlap halo already included. The channel and pyramid level from the config are already selected, so it is purely spatial: `(z, y, x)` for a 3-D run, `(y, x)` for 2-D. Dtype is the image's (e.g. `uint16`). |
215
+ | **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
216
+ | **Labels** | Only need to be unique **within the tile**. Don't try to make them globally unique — the merge step stitches objects across tile borders and renumbers everything to a contiguous `1..N` (`sequential_labels: true`). |
217
+ | **Shape** | Must match the input exactly — patchworks trims the halo off your output, so a wrong shape is an error. Don't crop or resize inside the function. |
218
+
219
+ That is the whole interface. Anything that turns an image tile into a label
220
+ image works: classic image processing, StarDist, a trained model, an external
221
+ binary you shell out to, …
222
+
223
+ ### Minimal example (no GPU, no deps beyond scikit-image)
224
+
225
+ ```python
226
+ # my_seg.py
227
+ import numpy as np
228
+ from skimage.measure import label
229
+
230
+ def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
231
+ """Threshold + connected components. Returns int32 labels (0 = bg)."""
232
+ from skimage.filters import gaussian, threshold_otsu
233
+
234
+ smooth = gaussian(tile, sigma=sigma, preserve_range=True)
235
+ thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
236
+ return label(smooth > thr).astype("int32")
237
+ ```
238
+
239
+ ```yaml
240
+ method: "custom"
241
+ label_name: "my_labels"
242
+ custom:
243
+ module: "my_seg" # import name (see "Make it importable")
244
+ function: "segment" # default is "segment"
245
+ kwargs: # optional — forwarded as segment(tile, **kwargs)
246
+ sigma: 1.5
247
+ ```
248
+
249
+ ### Real example: StarDist 3-D, with model caching
250
+
251
+ Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
252
+ own process so this matters less, but for local runs one process segments many
253
+ tiles — cache the model at module level (or with `functools.lru_cache`):
254
+
255
+ ```python
256
+ # stardist_seg.py
257
+ import numpy as np
258
+
259
+ _MODEL = None
260
+
261
+ def _model():
262
+ global _MODEL
263
+ if _MODEL is None: # loaded once per worker process
264
+ from stardist.models import StarDist3D
265
+ _MODEL = StarDist3D.from_pretrained("3D_demo")
266
+ return _MODEL
267
+
268
+ def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
269
+ from csbdeep.utils import normalize
270
+
271
+ labels, _ = _model().predict_instances(
272
+ normalize(tile), prob_thresh=prob_thresh
273
+ )
274
+ return labels.astype("int32")
275
+ ```
276
+
277
+ ```yaml
278
+ method: "custom"
279
+ label_name: "stardist"
280
+ custom:
281
+ module: "stardist_seg"
282
+ function: "segment"
283
+ kwargs:
284
+ prob_thresh: 0.5
285
+ ```
286
+
287
+ Using a GPU? The segment jobs already hold one (the `gres: "gpu:1"` request),
288
+ so just let your framework see it — nothing extra in the config.
289
+
290
+ ### Test it before you submit
291
+
292
+ Run your function on one real tile first — it catches shape/dtype bugs in
293
+ seconds instead of after a queue wait. Output must be integer, same shape, `0`
294
+ for background:
295
+
296
+ ```python
297
+ from patchworks import load_ome_zarr
298
+ from my_seg import segment
299
+
300
+ img = load_ome_zarr("results/image.zarr", channel=0, level=0)
301
+ tile = img[:, :512, :512].compute() # a small spatial block
302
+ out = segment(tile)
303
+
304
+ assert out.shape == tile.shape, (out.shape, tile.shape)
305
+ assert out.dtype.kind in "iu" # integer labels, not a float mask
306
+ print("objects in tile:", int(out.max()))
307
+ ```
308
+
309
+ ### Make it importable on the cluster
310
+
311
+ The segment job runs `import <module>`, so the module must be on the path. Pick
312
+ one:
313
+
314
+ 1. **Drop the file in `workflow/scripts/`** — Snakemake adds the script dir to
315
+ `sys.path`, so `module: "my_seg"` just works. Simplest for a single file.
316
+ 2. **Install it** into the run env (`pip install -e .`, `pixi add --pypi …`),
317
+ then use its import name. Best for a real package with dependencies.
318
+ 3. **Set `PYTHONPATH`** to the file's directory before launching Snakemake.
319
+
320
+ ### Cluster checklist
321
+
322
+ - **Dependencies:** the env that runs the **segment** jobs must have everything
323
+ your function imports (`pip`/`pixi add` it). A missing import or any crash
324
+ shows up in `logs/segment/<index>.log`, not the (empty) SLURM log.
325
+ - **Offline GPU nodes:** the built-in `fetch_model` prefetch covers Cellpose
326
+ only. If your function downloads weights/data on first use, fetch them once on
327
+ the **login node** (network access) so they land in shared `$HOME`; otherwise
328
+ the segment jobs fail with `Network is unreachable`. See *Troubleshooting*.
329
+ - **Memory / walltime:** tune `segment:` in `profile/slurm/config.yaml` for your
330
+ model (`mem_mb`, `runtime`) just as for Cellpose.
331
+ - Everything else — tiling, halos, empty-tile skipping, the zarr-native merge,
332
+ resume, and per-tile logs — is identical to a Cellpose run.
333
+
334
+ For full control (your own tiling/merge loop instead of the bundled rules), call
335
+ the public API directly — see *How it works* below.
336
+
197
337
  ## pixi (instead of conda)
198
338
 
199
339
  Conda is **not** required — Snakemake runs in whatever environment launches it.
@@ -223,10 +363,12 @@ prologue. The simplest path is a single shared env that the compute nodes see.
223
363
  | Symptom | Fix |
224
364
  |---------|-----|
225
365
  | `snakemake: command not found` | use `python -m snakemake` |
226
- | Segment jobs pend forever | wrong `slurm_partition`/`slurm_extra` GPU flag for your cluster |
366
+ | Segment jobs pend forever | wrong `slurm_partition`/GPU request; on scicore use `gres: "gpu:1"` |
367
+ | Segment dies, `Network is unreachable` | offline GPU nodes — the `fetch_model` localrule caches the model on the submit host first; if it still fails, your submit host has no network either (pre-download manually) |
227
368
  | `cellpose is not installed` in a job | the job's env lacks `patchworks[cellpose]` |
228
369
  | Reading the input fails | install the matching reader (`patchworks[imaris]`/`[bioio]` + a `bioio-*`) |
229
370
  | Out of GPU memory | smaller `tile_shape`, or `do_3D: false` |
371
+ | A job fails with an empty SLURM log | read `logs/segment/<index>.log` (per tile) or `logs/steps.log` — the real traceback is there |
230
372
  | Very slow | confirm GPU is used (`nvidia-smi`); try 2-D or a lower `level` |
231
373
 
232
374
  ## How it works (for the curious)
@@ -57,7 +57,11 @@ bioio = [
57
57
  # imaris reads .ims files natively (HDF5, no JVM) for OME-ZARR conversion.
58
58
  imaris = ["imaris-ims-file-reader"]
59
59
  # napari enables the interactive viewer plugin.
60
- napari = ["napari[all]"]
60
+ # - napari >= 0.5: NumPy 2.0 compatible (older 0.4.x uses np.array(copy=False),
61
+ # which raises under NumPy 2 -> ValidationError on Viewer()).
62
+ # - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
63
+ # into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
64
+ napari = ["napari[all]>=0.5.5", "lxml-html-clean"]
61
65
  # workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
62
66
  workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm"]
63
67
  dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
@@ -19,6 +19,11 @@ include: "rules/segment.smk"
19
19
  include: "rules/merge.smk"
20
20
 
21
21
 
22
+ # Runs on the submit host (has network) — never submitted to an offline GPU node.
23
+ localrules:
24
+ fetch_model,
25
+
26
+
22
27
  rule all:
23
28
  input:
24
29
  f"{WORK}/labels.done",
@@ -12,7 +12,7 @@ work_dir: "/path/to/results" # everything is written under here
12
12
  # image.zarr/labels/<name>/ — the segmentation (multi-scale), in the image
13
13
 
14
14
  # ---- conversion -------------------------------------------------------------
15
- reuse_pyramid: true # for .ims: copy its own pyramid (fast); else rebuild
15
+ reuse_pyramid: false # for .ims: copy its own pyramid (fast); else rebuild
16
16
  convert_chunks: null # null → patchworks' bounded auto chunks; or [c,z,y,x]
17
17
  shard: false # true → pack chunks into shards (zarr v3; fewer files)
18
18
  # Re-running reuses an existing image.zarr automatically (skips conversion).
@@ -29,10 +29,10 @@ skip_empty: true # skip background tiles
29
29
  empty_threshold: null # null → Otsu; or a number
30
30
 
31
31
  # ---- segmentation -----------------------------------------------------------
32
- method: "cellpose" # "cellpose" (GPU) or "threshold" (simple, no GPU; testing)
33
- label_name: "cellpose"
32
+ method: "cellpose" # "cellpose" (GPU), "threshold" (no GPU; testing), "custom"
33
+ label_name: "cellpose_labels"
34
34
  cellpose:
35
- model: "cyto3"
35
+ model: "nuclei"
36
36
  diameter: 30
37
37
  do_3D: true
38
38
  gpu: true
@@ -40,6 +40,13 @@ cellpose:
40
40
  # flow_threshold: 0.4
41
41
  # cellprob_threshold: 0.0
42
42
 
43
+ # Your own per-tile function (method: "custom"). See the "custom function"
44
+ # section of docs/guide/snakemake.md.
45
+ # custom:
46
+ # module: "my_seg" # importable on the cluster (workflow/scripts/, PYTHONPATH, or pip-installed)
47
+ # function: "segment" # def segment(tile: np.ndarray) -> np.ndarray (int32 labels)
48
+ # kwargs: {} # optional extra keyword args
49
+
43
50
  # ---- pyramid for the labels -------------------------------------------------
44
51
  pyramid_levels: 5
45
52
  pyramid_downscale: 2
@@ -7,7 +7,7 @@
7
7
  # On a cluster, put this project on a shared filesystem the compute nodes can
8
8
  # read: the SLURM jobs re-launch snakemake from this env's interpreter.
9
9
 
10
- [project]
10
+ [workspace]
11
11
  name = "patchworks-workflow"
12
12
  channels = ["conda-forge"]
13
13
  platforms = ["linux-64"]
@@ -33,11 +33,14 @@ set-resources:
33
33
  mem_mb: 32000
34
34
  runtime: 120
35
35
  segment:
36
- # one GPU per tile — this is what spreads Cellpose across GPUs
36
+ # one GPU per tile — this is what spreads Cellpose across GPUs.
37
+ # Use the plugin's native `gres` resource → emits --gres=gpu:1, which is
38
+ # how scicore binds a CUDA device. (The `gpu:` resource emits --gpus and
39
+ # leaves the job without a device.)
37
40
  slurm_partition: "rtx4090"
38
- gpu: 1
41
+ gres: "gpu:1"
39
42
  qos: "rtx4090-6hours" # scicore: <partition>-<duration> QOS
40
- mem_mb: 32000 # plenty — a tile used ~1G; the failure was a Python error
43
+ mem_mb: 32000 # plenty — a tile used ~1G
41
44
  cpus_per_task: 4
42
45
  runtime: 360 # 6 hours — must match the QOS, NOT 120 (=2h → killed early)
43
46
  merge:
@@ -20,6 +20,10 @@ STAGE_OK = f"{STAGE}.done"
20
20
  LOGS = f"{WORK}/logs"
21
21
  STEPLOG = f"{LOGS}/steps.log"
22
22
 
23
+ # Marker that the segmentation model is cached locally. Produced by a local
24
+ # rule (runs on the networked submit host) so offline GPU nodes never download.
25
+ MODEL_OK = f"{WORK}/model.ready"
26
+
23
27
 
24
28
  def occupied_done(wildcards):
25
29
  """Per-tile markers for the occupied tiles (resolved after the checkpoint)."""
@@ -1,5 +1,20 @@
1
1
  # Plan tiles (checkpoint) and segment each tile on a GPU.
2
2
 
3
+
4
+ rule fetch_model:
5
+ """Cache the segmentation model on the (networked) submit host.
6
+
7
+ Declared local (see ``localrules`` in the Snakefile) so it never runs on an
8
+ offline GPU node — Cellpose downloads its weights here, into shared $HOME.
9
+ """
10
+ output:
11
+ touch(MODEL_OK),
12
+ log:
13
+ f"{LOGS}/fetch_model.log",
14
+ script:
15
+ "../scripts/fetch_model.py"
16
+
17
+
3
18
  checkpoint prepare:
4
19
  input:
5
20
  IMAGE_OK,
@@ -18,6 +33,7 @@ rule segment:
18
33
  tiles=TILES,
19
34
  stage=STAGE_OK,
20
35
  image=IMAGE_OK,
36
+ model=MODEL_OK,
21
37
  output:
22
38
  f"{WORK}/seg/{{index}}.done",
23
39
  log:
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import json
11
11
  import logging
12
12
  import sys
13
+ from functools import partial
13
14
  from pathlib import Path
14
15
 
15
16
  from patchworks import load_ome_zarr
@@ -124,8 +125,9 @@ def build_fn(cfg):
124
125
  Parameters
125
126
  ----------
126
127
  cfg : dict
127
- Snakemake config. ``method`` selects ``"cellpose"`` (default) or a
128
- simple ``"threshold"`` (handy for testing / no-GPU runs).
128
+ Snakemake config. ``method`` selects ``"cellpose"`` (default), a simple
129
+ ``"threshold"`` (testing / no-GPU), or ``"custom"`` to import your own
130
+ function (``cfg["custom"] = {module, function, kwargs}``).
129
131
 
130
132
  Returns
131
133
  -------
@@ -133,6 +135,21 @@ def build_fn(cfg):
133
135
  ``(ndarray) -> ndarray`` returning integer labels.
134
136
  """
135
137
  method = cfg.get("method", "cellpose")
138
+ if method == "custom":
139
+ # Import a user-provided function, e.g.
140
+ # custom: {module: my_seg, function: segment, kwargs: {...}}
141
+ # The module must be importable on the cluster (a file in
142
+ # workflow/scripts/, on PYTHONPATH, or an installed package).
143
+ import importlib
144
+
145
+ spec = cfg["custom"]
146
+ fn = getattr(
147
+ importlib.import_module(spec["module"]),
148
+ spec.get("function", "segment"),
149
+ )
150
+ kwargs = spec.get("kwargs") or {}
151
+ return partial(fn, **kwargs) if kwargs else fn
152
+
136
153
  if method == "threshold":
137
154
 
138
155
  def fn(tile):
@@ -0,0 +1,21 @@
1
+ """Snakemake (local) script: cache the segmentation model before segmenting.
2
+
3
+ Runs on the submit host, which has network access, so the offline GPU nodes
4
+ never try to download Cellpose weights at run time (they read the shared
5
+ ``$HOME/.cellpose`` cache instead).
6
+ """
7
+
8
+ from _pw import start_log
9
+
10
+ start_log(snakemake.log[0]) # noqa: F821
11
+ cfg = snakemake.config # noqa: F821
12
+
13
+ if cfg.get("method", "cellpose") == "cellpose":
14
+ # _get_model downloads + caches the weights keyed by (model, gpu).
15
+ from patchworks.plugins.cellpose import _get_model
16
+
17
+ model = cfg["cellpose"]["model"]
18
+ _get_model({"model": model, "gpu": False})
19
+ print(f"[patchworks] cached segmentation model: {model}")
20
+ else:
21
+ print(f"[patchworks] method={cfg.get('method')!r}; no model to prefetch")
File without changes
File without changes
File without changes
File without changes
File without changes