patchworks 2.6.1__tar.gz → 2.6.2__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 (101) hide show
  1. {patchworks-2.6.1 → patchworks-2.6.2}/PKG-INFO +1 -1
  2. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/gpu_distributed.md +5 -0
  3. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/snakemake.md +31 -2
  4. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/tiling.md +17 -0
  5. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_chunks.py +33 -7
  6. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_allocation.py +42 -1
  7. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_core.py +9 -1
  8. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_run_multi.py +27 -0
  9. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/README.md +5 -0
  10. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/profile/slurm/config.yaml +9 -1
  11. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/run_multi.py +18 -0
  12. {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/docs.yml +0 -0
  13. {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/lint.yml +0 -0
  14. {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/release.yml +0 -0
  15. {patchworks-2.6.1 → patchworks-2.6.2}/.gitignore +0 -0
  16. {patchworks-2.6.1 → patchworks-2.6.2}/.markdownlint-cli2.yaml +0 -0
  17. {patchworks-2.6.1 → patchworks-2.6.2}/LICENSE +0 -0
  18. {patchworks-2.6.1 → patchworks-2.6.2}/README.md +0 -0
  19. {patchworks-2.6.1 → patchworks-2.6.2}/cliff.toml +0 -0
  20. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/chunks.md +0 -0
  21. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/cluster.md +0 -0
  22. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/io.md +0 -0
  23. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/merge_tile_labels.md +0 -0
  24. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/cellpose.md +0 -0
  25. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/dog.md +0 -0
  26. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/napari.md +0 -0
  27. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/ome_zarr.md +0 -0
  28. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/postprocess.md +0 -0
  29. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/relabel.md +0 -0
  30. {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/tile_process.md +0 -0
  31. {patchworks-2.6.1 → patchworks-2.6.2}/docs/assets/logo.png +0 -0
  32. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_2d.md +0 -0
  33. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_2d.py +0 -0
  34. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_3d.md +0 -0
  35. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_3d.py +0 -0
  36. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/custom.md +0 -0
  37. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/custom_method.py +0 -0
  38. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/dog.md +0 -0
  39. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/dog.py +0 -0
  40. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/standalone_merge.md +0 -0
  41. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/stardist.md +0 -0
  42. {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/stardist_2d.py +0 -0
  43. {patchworks-2.6.1 → patchworks-2.6.2}/docs/getting_started.md +0 -0
  44. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/custom_segmentation.md +0 -0
  45. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/label_relations.md +0 -0
  46. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/measurements.md +0 -0
  47. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/merging.md +0 -0
  48. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/ome_zarr_napari.md +0 -0
  49. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/performance.md +0 -0
  50. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/pitfalls.md +0 -0
  51. {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/skip_empty.md +0 -0
  52. {patchworks-2.6.1 → patchworks-2.6.2}/docs/index.md +0 -0
  53. {patchworks-2.6.1 → patchworks-2.6.2}/mkdocs.yml +0 -0
  54. {patchworks-2.6.1 → patchworks-2.6.2}/pyproject.toml +0 -0
  55. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/__init__.py +0 -0
  56. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_cluster.py +0 -0
  57. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_core.py +0 -0
  58. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_distributed.py +0 -0
  59. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_gpu.py +0 -0
  60. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_io.py +0 -0
  61. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_merge.py +0 -0
  62. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_notify.py +0 -0
  63. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_occupancy.py +0 -0
  64. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_postprocess.py +0 -0
  65. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_progress.py +0 -0
  66. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_relabel.py +0 -0
  67. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_relations.py +0 -0
  68. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/__init__.py +0 -0
  69. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/cellpose.py +0 -0
  70. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/dog.py +0 -0
  71. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/napari.py +0 -0
  72. {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/ome_zarr.py +0 -0
  73. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_distributed.py +0 -0
  74. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_dog.py +0 -0
  75. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_gpu.py +0 -0
  76. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_napari.py +0 -0
  77. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_notify.py +0 -0
  78. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_occupancy.py +0 -0
  79. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_ome_zarr.py +0 -0
  80. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_postprocess.py +0 -0
  81. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_progress.py +0 -0
  82. {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_relations.py +0 -0
  83. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/Snakefile +0 -0
  84. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/common.yaml +0 -0
  85. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config.yaml +0 -0
  86. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_cilia.yaml +0 -0
  87. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_cyto.yaml +0 -0
  88. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_nuclei.yaml +0 -0
  89. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/multi.yaml +0 -0
  90. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/pixi.toml +0 -0
  91. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/common.smk +0 -0
  92. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/convert.smk +0 -0
  93. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/merge.smk +0 -0
  94. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/segment.smk +0 -0
  95. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/_pw.py +0 -0
  96. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/build_occupancy.py +0 -0
  97. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/convert.py +0 -0
  98. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/fetch_model.py +0 -0
  99. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/merge.py +0 -0
  100. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/prepare_tiles.py +0 -0
  101. {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 2.6.1
3
+ Version: 2.6.2
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
@@ -79,6 +79,11 @@ own outright. `auto_tile_shape` then sizes each tile to at most half of that
79
79
  budget; `auto_tile_shape_cellpose` uses Cellpose's own memory model instead
80
80
  (roughly 20× the raw tile bytes, plus ~2 GiB for the model).
81
81
 
82
+ "Raw tile bytes" counts every channel a tile carries, so pass `n_channels=2`
83
+ when feeding Cellpose a cyto+nuclei pair — see
84
+ [Multi-channel tiles](tiling.md#multi-channel-tiles). The Snakemake workflow
85
+ does this for you whenever `nuclei_channel` is set.
86
+
82
87
  The device is resolved from `CUDA_VISIBLE_DEVICES`. This matters on
83
88
  multi-GPU nodes: NVML enumerates **every** GPU regardless of `--gres=gpu:1`,
84
89
  so querying index 0 unconditionally would read a different card's free memory
@@ -109,6 +109,15 @@ sequential_labels: true # renumber labels to a contiguous 1..N
109
109
  explicit z (like `[16, 1024, 1024]`) tiles in z instead. `prepare` logs
110
110
  which regime it picked.
111
111
 
112
+ `"auto"` also caps the tile to the **host** RAM available to the job, not
113
+ just VRAM — a `do_3D` tile that comfortably fits a big GPU can still be
114
+ too large for the SLURM/cgroup memory the job was actually granted, and
115
+ that shows up as a plain `SIGKILL`, not a catchable CUDA-OOM error.
116
+ Because `prepare` runs on a CPU node, it checks *its own* grant as a
117
+ stand-in for `segment`'s — keep `prepare`'s and `segment`'s `mem_mb` in
118
+ `profile/slurm/config.yaml` equal, or the estimate is sized against the
119
+ wrong job's budget.
120
+
112
121
  !!! tip "Use a per-axis `overlap`"
113
122
  A scalar halo is applied to every axis. On a `[16, 1024, 1024]` tile,
114
123
  `overlap: 30` reads `76 × 1084 × 1084` to keep `16 × 1024 × 1024` — 5.3×
@@ -323,13 +332,33 @@ follow from that:
323
332
 
324
333
  - A tile holds twice the bytes. `tile_shape: "auto"` accounts for this — it is
325
334
  told the tile carries two channels and shrinks each spatial side by ~1/√2,
326
- so the tile still fits the same VRAM budget. A **hand-set** `tile_shape`
327
- sized to fill a GPU has no such protection and needs halving yourself.
335
+ so the tile still fits the same VRAM *and* host-RAM budget (see the "Tile
336
+ size vs runtime" tip above). A **hand-set** `tile_shape` sized to fill a
337
+ GPU has no such protection and needs halving yourself.
328
338
  - The translation is version-specific. Cellpose 3 gets `channels: [1, 2]`
329
339
  (1-based into the channel axis, `0` = grayscale); Cellpose 4 (cpsam) dropped
330
340
  `channels` entirely and simply reads both. Either is overridable by setting
331
341
  `channels:` or `channel_axis:` in the `cellpose:` block.
332
342
 
343
+ !!! warning "In a `multi.yaml` run, pin `tile_shape` explicitly"
344
+
345
+ `label_relations` requires its two label arrays to share a chunk layout,
346
+ and that layout comes from `tile_shape`. Giving *one* config a
347
+ `nuclei_channel` while the group uses `tile_shape: "auto"` produces a
348
+ **smaller** tile for that config only — so the label groups end up chunked
349
+ differently and the relations step fails, after every segmentation has
350
+ already run.
351
+
352
+ `run_multi`'s cross-config check compares the configured values, and
353
+ `"auto" == "auto"`, so it flags this case specifically. Fix it by setting
354
+ one explicit `tile_shape` in `common.yaml`, sized for the two-channel
355
+ config (roughly each spatial side ÷ √2 versus what you would use for a
356
+ single channel), so every config shares it.
357
+
358
+ If you do not need that segmentation related to the others, the alternative is
359
+ to run it on its own against the same `work_dir` and leave it out of
360
+ `multi.yaml`.
361
+
333
362
  `nuclei_channel` applies to the SLURM/Snakemake path. The single-process
334
363
  `tile_process` API still takes one `channel`.
335
364
 
@@ -57,6 +57,23 @@ tile_process("image.zarr", fn, tile_shape=tile_fn)
57
57
  The callable is called with `(shape, dtype)` at runtime, after the image is
58
58
  loaded — useful when you don't know the image shape in advance.
59
59
 
60
+ ### Multi-channel tiles
61
+
62
+ Both sizers take `n_channels` (default 1) and charge it per voxel. Use it when
63
+ a tile carries more than one channel while its *geometry* stays spatial — the
64
+ case the Snakemake workflow's
65
+ [`nuclei_channel`](snakemake.md#giving-cellpose-a-nuclei-channel) creates, where
66
+ Cellpose is handed a cyto+nuclei pair but still returns one label per voxel:
67
+
68
+ ```python
69
+ # Same VRAM budget, so each spatial side shrinks by ~1/√2
70
+ auto_tile_shape_cellpose(shape, dtype, diameter=30, use_gpu=True, n_channels=2)
71
+ ```
72
+
73
+ Leaving it at 1 for a 2-channel tile budgets for half the bytes the tile
74
+ actually holds, which surfaces as an out-of-memory error in the first tile
75
+ rather than as anything about channels.
76
+
60
77
  ## Overlap
61
78
 
62
79
  Methods that need spatial context (Cellpose, StarDist, U-Net) produce wrong
@@ -281,11 +281,15 @@ def auto_tile_shape(
281
281
  target_bytes:
282
282
  Memory ceiling per tile. Default 64 MiB.
283
283
  use_gpu:
284
- Size tiles against GPU VRAM rather than host RAM.
284
+ Size tiles against GPU VRAM rather than host RAM. Still capped by
285
+ host RAM too -- see ``available_memory``.
285
286
  gpu_memory:
286
287
  Available GPU VRAM in bytes; auto-queried when None.
287
288
  available_memory:
288
- Available host RAM in bytes; auto-queried when None.
289
+ Available host RAM in bytes; auto-queried when None. Consulted even
290
+ when ``use_gpu`` is set: the tile has to be loaded into host memory
291
+ before (and often after) it reaches the GPU, so it must fit both
292
+ budgets, not just VRAM.
289
293
  n_workers:
290
294
  Number of parallel workers (divides the RAM budget).
291
295
  n_channels:
@@ -316,7 +320,13 @@ def auto_tile_shape(
316
320
  n_spatial = min(3, len(shape))
317
321
 
318
322
  if use_gpu:
319
- mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
323
+ # A tile that fits VRAM still has to be decoded into (and often
324
+ # copied back out of) host RAM first, so a GPU with more memory
325
+ # than the job's own host allocation must not produce a tile the
326
+ # job itself can't hold -- take whichever budget is tighter.
327
+ gpu_mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
328
+ host_mem = available_memory or _get_available_memory()
329
+ mem = min(gpu_mem, host_mem)
320
330
  budget = min(target_bytes * 2, mem // 2)
321
331
  else:
322
332
  mem = available_memory or _get_available_memory()
@@ -395,13 +405,23 @@ def auto_tile_shape_cellpose(
395
405
  do_3D:
396
406
  Whether Cellpose will run in 3-D mode.
397
407
  use_gpu:
398
- Size tiles for GPU VRAM.
408
+ Size tiles for GPU VRAM. Still capped by host RAM too -- see
409
+ ``available_memory``.
399
410
  gpu_memory, available_memory, n_workers:
400
- Memory parameters (auto-queried when None).
411
+ Memory parameters (auto-queried when None). ``available_memory`` is
412
+ consulted even when ``use_gpu`` is set: Cellpose's ``do_3D``
413
+ preprocessing (orthogonal-view flows, mask assembly) copies the tile
414
+ through host memory, so VRAM headroom alone doesn't guarantee the
415
+ job's own host allocation can hold it.
401
416
  model_memory_bytes:
402
417
  Memory consumed by the Cellpose model weights (default 2 GiB).
403
418
  cellpose_memory_factor:
404
- Cellpose allocates roughly this multiple of raw input bytes (default 20×).
419
+ Cellpose allocates roughly this multiple of raw input bytes (default
420
+ 20×). Applied against whichever of VRAM/host RAM is tighter --
421
+ ``ponytail:`` this reuses one factor for both budgets as an
422
+ approximation; if it proves off in practice, calibrate a separate
423
+ host-side factor from a SLURM job's peak RSS (``seff``/
424
+ ``/usr/bin/time -v``) and pass it alongside this one.
405
425
  n_channels:
406
426
  Channels each tile carries (default 1). Above 1 the per-voxel cost
407
427
  scales with it, so the tile shrinks accordingly -- e.g. the workflow's
@@ -429,7 +449,13 @@ def auto_tile_shape_cellpose(
429
449
  itemsize = np.dtype(dtype).itemsize * n_channels
430
450
 
431
451
  if use_gpu:
432
- total_mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
452
+ # A tile that fits VRAM still has to be decoded into (and often
453
+ # copied back out of) host RAM first, so a GPU with more memory
454
+ # than the job's own host allocation must not produce a tile the
455
+ # job itself can't hold -- take whichever budget is tighter.
456
+ gpu_mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
457
+ host_mem = available_memory or _get_available_memory()
458
+ total_mem = min(gpu_mem, host_mem)
433
459
  else:
434
460
  total_mem = (available_memory or _get_available_memory()) // n_workers
435
461
 
@@ -8,7 +8,11 @@ checks that would have caught it.
8
8
 
9
9
  import numpy as np
10
10
 
11
- from patchworks import cpu_allocation, safe_worker_count
11
+ from patchworks import (
12
+ auto_tile_shape_cellpose,
13
+ cpu_allocation,
14
+ safe_worker_count,
15
+ )
12
16
  from patchworks._chunks import _get_available_memory
13
17
 
14
18
  GIB = 1024**3
@@ -76,3 +80,40 @@ def test_worker_count_is_bounded_by_the_allocation(monkeypatch):
76
80
  n = safe_worker_count(chunk_nbytes * 40, fn_overhead=3)
77
81
  assert n < 32, "must not size itself to the core count when RAM is tighter"
78
82
  assert n >= 1
83
+
84
+
85
+ def test_gpu_tile_sizing_is_bounded_by_the_host_allocation(monkeypatch):
86
+ """A big GPU must not excuse a tile the job's own host RAM can't hold.
87
+
88
+ This is the concrete failure a `do_3D` nuclei segmentation hit: an ample
89
+ GPU (24 GiB) sized the tile against VRAM alone, and the job -- granted
90
+ only 1 GiB of host RAM here -- was SIGKILLed loading it, unrelated to
91
+ ``nuclei_channel``. The sizer must take whichever budget is tighter.
92
+ """
93
+ monkeypatch.delenv("SLURM_MEM_PER_CPU", raising=False)
94
+ monkeypatch.setenv("SLURM_MEM_PER_NODE", str(1024)) # 1 GiB
95
+ monkeypatch.setattr(
96
+ "patchworks._chunks._cgroup_memory_limit", lambda: 512 * GIB
97
+ )
98
+
99
+ tile = auto_tile_shape_cellpose(
100
+ (128, 2048, 2048),
101
+ "uint16",
102
+ diameter=30,
103
+ do_3D=True,
104
+ use_gpu=True,
105
+ gpu_memory=24 * GIB,
106
+ )
107
+ generous = auto_tile_shape_cellpose(
108
+ (128, 2048, 2048),
109
+ "uint16",
110
+ diameter=30,
111
+ do_3D=True,
112
+ use_gpu=True,
113
+ gpu_memory=24 * GIB,
114
+ available_memory=64 * GIB,
115
+ )
116
+ assert np.prod(tile) < np.prod(generous), (
117
+ "the 1 GiB host grant must shrink the tile below what the same "
118
+ "24 GiB GPU would otherwise allow"
119
+ )
@@ -410,7 +410,15 @@ def test_auto_tile_shape_charges_for_extra_channels():
410
410
  assert np.prod(two) * 2 <= np.prod(one)
411
411
  assert np.prod(two) * 2 >= np.prod(one) * 0.9
412
412
 
413
- kw = dict(diameter=30, do_3D=True, use_gpu=True, gpu_memory=24 * 1024**3)
413
+ kw = dict(
414
+ diameter=30,
415
+ do_3D=True,
416
+ use_gpu=True,
417
+ gpu_memory=24 * 1024**3,
418
+ # Generous on purpose: this test is about GPU-vs-channel scaling,
419
+ # not the host-RAM ceiling, so host RAM must stay non-binding here.
420
+ available_memory=64 * 1024**3,
421
+ )
414
422
  cp_one = auto_tile_shape_cellpose(shape, dtype, **kw)
415
423
  cp_two = auto_tile_shape_cellpose(shape, dtype, n_channels=2, **kw)
416
424
  assert np.prod(cp_two) * 2 <= np.prod(cp_one)
@@ -158,3 +158,30 @@ def test_occupancy_is_not_rebuilt_by_the_driver():
158
158
  src = (_workflow_dir() / "scripts" / "run_multi.py").read_text()
159
159
  assert "build_occupancy_map(" not in src
160
160
  assert "occupancy.zarr" in src
161
+
162
+
163
+ def test_auto_tile_shape_with_a_lone_nuclei_channel_is_refused():
164
+ """Matching `tile_shape` *values* are not enough when one config is 2-ch.
165
+
166
+ "auto" == "auto" passes the plain equality check, but the sizer charges
167
+ per channel, so the nuclei_channel config gets a smaller tile. The label
168
+ groups then disagree on chunk layout and label_relations raises -- after
169
+ every segmentation has already run, which is the expensive way to find out.
170
+ """
171
+ paths = [Path("a.yaml"), Path("b.yaml")]
172
+ base = {"work_dir": "/w", "tile_shape": "auto", "level": 0}
173
+
174
+ bad = [
175
+ {**base, "label_name": "a", "channel": 0, "nuclei_channel": 1},
176
+ {**base, "label_name": "b", "channel": 2},
177
+ ]
178
+ with pytest.raises(SystemExit):
179
+ _validate_configs(paths, bad)
180
+
181
+ # Same pair with one explicit shape is fine: both get that tile.
182
+ pinned = [{**c, "tile_shape": [16, 512, 512]} for c in bad]
183
+ assert _validate_configs(paths, pinned) == "/w"
184
+
185
+ # And "auto" is fine when every config carries the same channel count.
186
+ both = [{**bad[0]}, {**bad[1], "nuclei_channel": 3}]
187
+ assert _validate_configs(paths, both) == "/w"
@@ -68,6 +68,11 @@ Edit `config/config.yaml` (input, output dir, channel, tile shape, Cellpose
68
68
  model/diameter/`do_3D`, …) and `profile/slurm/config.yaml` (partitions,
69
69
  account, GPU request).
70
70
 
71
+ Channel indices are **0-based**. Set `nuclei_channel` alongside `channel` to
72
+ hand Cellpose the nuclear stain as a second input, which usually improves
73
+ cytoplasm segmentation — see
74
+ [Giving Cellpose a nuclei channel](https://imcf.one/patchworks/guide/snakemake/#giving-cellpose-a-nuclei-channel).
75
+
71
76
  ## Run
72
77
 
73
78
  ```bash
@@ -67,6 +67,13 @@ set-resources:
67
67
  prepare:
68
68
  # Cheap now: it reduces the already-built occupancy map over each tile's
69
69
  # footprint instead of touching the image.
70
+ #
71
+ # tile_shape: "auto" now also sizes against a host-RAM budget, and it
72
+ # reads *this job's* grant as a stand-in for segment's -- prepare runs on
73
+ # a CPU node and can't query segment's GPU-node grant directly. Keep this
74
+ # equal to segment's mem_mb below: lower and tiles come out needlessly
75
+ # small; higher and the sizer budgets against more host RAM than segment
76
+ # will actually get, undoing the point of the host-RAM check.
70
77
  mem_mb: "attempt * 32000"
71
78
  runtime: 120
72
79
  segment:
@@ -80,7 +87,8 @@ set-resources:
80
87
  # A job now processes `tiles_per_job` tiles sequentially, so both memory
81
88
  # and runtime scale with that setting — raise it there and re-check here.
82
89
  # The old "a tile used ~1G" note predates tile_shape: "auto", which sizes
83
- # tiles against the real GPU and makes them far bigger.
90
+ # tiles against the real GPU and makes them far bigger. If you raise
91
+ # this, raise prepare's mem_mb above to match (see its comment).
84
92
  mem_mb: "attempt * 32000"
85
93
  cpus_per_task: 4
86
94
  runtime: 360 # 6 hours — must match the QOS, NOT 120 (=2h → killed early)
@@ -228,6 +228,24 @@ def _validate_configs(paths: list[Path], cfgs: list[dict]) -> str:
228
228
  f"share a chunk layout; got {_spread(key)}"
229
229
  )
230
230
 
231
+ # `tile_shape: "auto"` is identical as a *value* across configs while
232
+ # producing different tiles: the sizer charges per channel, so a config
233
+ # with nuclei_channel gets a smaller one. The label groups then disagree on
234
+ # chunk layout and label_relations raises -- after every segmentation has
235
+ # run. Matching values are not enough here, so check the inputs that feed
236
+ # the sizer instead.
237
+ if {repr(cfg.get("tile_shape", "auto")) for cfg in cfgs} == {repr("auto")}:
238
+ if len({cfg.get("nuclei_channel") is not None for cfg in cfgs}) != 1:
239
+ problems.append(
240
+ 'tile_shape: "auto" sizes a nuclei_channel config smaller '
241
+ "(a tile carries two channels), so the label groups would "
242
+ "not share a chunk layout and label_relations would fail "
243
+ f"after every segmentation had run; got "
244
+ f"{_spread('nuclei_channel')}. Set one explicit tile_shape in "
245
+ "the file `common:` points at, sized for the two-channel "
246
+ "config."
247
+ )
248
+
231
249
  # Phase A converts once, from the first config. Anything `convert` reads
232
250
  # out of a later config is therefore silently ignored -- someone setting
233
251
  # `shard: true` on the second config and watching a million files appear
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes