patchworks 2.6.11__tar.gz → 2.7.0__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 (116) hide show
  1. {patchworks-2.6.11 → patchworks-2.7.0}/PKG-INFO +2 -1
  2. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/ome_zarr.md +12 -0
  3. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/snakemake.md +138 -7
  4. {patchworks-2.6.11 → patchworks-2.7.0}/pyproject.toml +10 -1
  5. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_chunks.py +29 -2
  6. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_io.py +15 -4
  7. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/cellpose.py +27 -1
  8. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/ome_zarr.py +440 -162
  9. patchworks-2.7.0/tests/ngff_schemas/0.4/image.schema +233 -0
  10. patchworks-2.7.0/tests/ngff_schemas/0.4/label.schema +77 -0
  11. patchworks-2.7.0/tests/ngff_schemas/0.4/ome.schema +17 -0
  12. patchworks-2.7.0/tests/ngff_schemas/0.5/_version.schema +10 -0
  13. patchworks-2.7.0/tests/ngff_schemas/0.5/image.schema +268 -0
  14. patchworks-2.7.0/tests/ngff_schemas/0.5/label.schema +91 -0
  15. patchworks-2.7.0/tests/ngff_schemas/0.5/ome.schema +33 -0
  16. patchworks-2.7.0/tests/ngff_schemas/README.md +16 -0
  17. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_allocation.py +57 -0
  18. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_cellpose.py +24 -0
  19. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_ome_zarr.py +328 -0
  20. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_pw.py +101 -0
  21. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_run_multi.py +75 -0
  22. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/common.yaml +6 -0
  23. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config.yaml +14 -0
  24. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/common.smk +9 -2
  25. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/_pw.py +76 -6
  26. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/build_occupancy.py +5 -1
  27. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/convert.py +8 -2
  28. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/merge.py +28 -1
  29. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/prepare_tiles.py +12 -0
  30. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/run_multi.py +1 -0
  31. {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/docs.yml +0 -0
  32. {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/lint.yml +0 -0
  33. {patchworks-2.6.11 → patchworks-2.7.0}/.github/workflows/release.yml +0 -0
  34. {patchworks-2.6.11 → patchworks-2.7.0}/.gitignore +0 -0
  35. {patchworks-2.6.11 → patchworks-2.7.0}/.markdownlint-cli2.yaml +0 -0
  36. {patchworks-2.6.11 → patchworks-2.7.0}/LICENSE +0 -0
  37. {patchworks-2.6.11 → patchworks-2.7.0}/README.md +0 -0
  38. {patchworks-2.6.11 → patchworks-2.7.0}/cliff.toml +0 -0
  39. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/chunks.md +0 -0
  40. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/cluster.md +0 -0
  41. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/io.md +0 -0
  42. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/merge_tile_labels.md +0 -0
  43. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/cellpose.md +0 -0
  44. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/dog.md +0 -0
  45. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/plugins/napari.md +0 -0
  46. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/postprocess.md +0 -0
  47. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/relabel.md +0 -0
  48. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/tile_process.md +0 -0
  49. {patchworks-2.6.11 → patchworks-2.7.0}/docs/api/volume_filter.md +0 -0
  50. {patchworks-2.6.11 → patchworks-2.7.0}/docs/assets/logo.png +0 -0
  51. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_2d.md +0 -0
  52. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_2d.py +0 -0
  53. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_3d.md +0 -0
  54. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/cellpose_3d.py +0 -0
  55. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/custom.md +0 -0
  56. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/custom_method.py +0 -0
  57. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/dog.md +0 -0
  58. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/dog.py +0 -0
  59. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/standalone_merge.md +0 -0
  60. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/stardist.md +0 -0
  61. {patchworks-2.6.11 → patchworks-2.7.0}/docs/examples/stardist_2d.py +0 -0
  62. {patchworks-2.6.11 → patchworks-2.7.0}/docs/getting_started.md +0 -0
  63. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/custom_segmentation.md +0 -0
  64. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/gpu_distributed.md +0 -0
  65. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/label_relations.md +0 -0
  66. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/measurements.md +0 -0
  67. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/merging.md +0 -0
  68. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/ome_zarr_napari.md +0 -0
  69. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/performance.md +0 -0
  70. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/pitfalls.md +0 -0
  71. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/skip_empty.md +0 -0
  72. {patchworks-2.6.11 → patchworks-2.7.0}/docs/guide/tiling.md +0 -0
  73. {patchworks-2.6.11 → patchworks-2.7.0}/docs/index.md +0 -0
  74. {patchworks-2.6.11 → patchworks-2.7.0}/mkdocs.yml +0 -0
  75. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/__init__.py +0 -0
  76. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_cluster.py +0 -0
  77. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_core.py +0 -0
  78. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_distributed.py +0 -0
  79. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_gpu.py +0 -0
  80. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_merge.py +0 -0
  81. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_notify.py +0 -0
  82. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_occupancy.py +0 -0
  83. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_postprocess.py +0 -0
  84. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_progress.py +0 -0
  85. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_relabel.py +0 -0
  86. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_relations.py +0 -0
  87. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/_volume_filter.py +0 -0
  88. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/__init__.py +0 -0
  89. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/dog.py +0 -0
  90. {patchworks-2.6.11 → patchworks-2.7.0}/src/patchworks/plugins/napari.py +0 -0
  91. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_core.py +0 -0
  92. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_distributed.py +0 -0
  93. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_dog.py +0 -0
  94. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_gpu.py +0 -0
  95. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_napari.py +0 -0
  96. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_notify.py +0 -0
  97. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_occupancy.py +0 -0
  98. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_postprocess.py +0 -0
  99. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_progress.py +0 -0
  100. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_relations.py +0 -0
  101. {patchworks-2.6.11 → patchworks-2.7.0}/tests/test_volume_filter.py +0 -0
  102. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/README.md +0 -0
  103. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/Snakefile +0 -0
  104. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_cilia.yaml +0 -0
  105. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_cyto.yaml +0 -0
  106. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/config_nuclei.yaml +0 -0
  107. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/config/multi.yaml +0 -0
  108. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/pixi.toml +0 -0
  109. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/profile/slurm/config.yaml +0 -0
  110. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/convert.smk +0 -0
  111. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/merge.smk +0 -0
  112. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/rules/segment.smk +0 -0
  113. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/fetch_model.py +0 -0
  114. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/relate.py +0 -0
  115. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/segment_tile.py +0 -0
  116. {patchworks-2.6.11 → patchworks-2.7.0}/workflow/scripts/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 2.6.11
3
+ Version: 2.7.0
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
@@ -57,6 +57,7 @@ Requires-Dist: cellpose<4,>=3.0; extra == 'cellpose3'
57
57
  Provides-Extra: cellpose4
58
58
  Requires-Dist: cellpose>=4; extra == 'cellpose4'
59
59
  Provides-Extra: dev
60
+ Requires-Dist: jsonschema>=4.18; extra == 'dev'
60
61
  Requires-Dist: psutil; extra == 'dev'
61
62
  Requires-Dist: pytest; extra == 'dev'
62
63
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -25,6 +25,10 @@ resolution, matching anisotropic microscopy stacks.
25
25
 
26
26
  ::: patchworks.plugins.ome_zarr.register_labels
27
27
 
28
+ ## reshard_level
29
+
30
+ ::: patchworks.plugins.ome_zarr.reshard_level
31
+
28
32
  ## read_pixel_size
29
33
 
30
34
  ::: patchworks.plugins.ome_zarr.read_pixel_size
@@ -37,6 +41,14 @@ matches the store, and reads both.
37
41
 
38
42
  ::: patchworks.plugins.ome_zarr.ngff_version
39
43
 
44
+ Every writer takes an `ngff_version=` keyword: `"auto"` (default) follows the
45
+ installed zarr — 0.5 on v3, 0.4 on v2 — and `"0.4"` pins the older, zarr-v2
46
+ layout. Writing into an existing store always follows *that store's* format,
47
+ so a label pyramid added later can never disagree with the image it sits in.
48
+ NGFF 0.6 is released but not written: RFC-5 replaces `axes` with
49
+ `coordinateSystems` and requires `input`/`output` on every coordinate
50
+ transformation, and no reader supports it yet.
51
+
40
52
  ::: patchworks.plugins.ome_zarr.read_ngff_attr
41
53
 
42
54
  ::: patchworks.plugins.ome_zarr.write_ngff_attrs
@@ -50,7 +50,9 @@ work_dir: "/scratch/results" # everything is written here
50
50
  # conversion (input → pyramidal OME-ZARR)
51
51
  reuse_pyramid: true # .ims: copy its own pyramid (fast)
52
52
  convert_chunks: null # null → bounded auto chunks; or [c,z,y,x]
53
- shard: false # true → pack chunks into shards (fewer files)
53
+ shard: false # true → pack chunks into shards (fewer files);
54
+ # covers the image and the label pyramids
55
+ ngff_version: "auto" # OME-ZARR version: "auto" (0.5), or "0.4"
54
56
 
55
57
  # tiling
56
58
  channel: 0 # channel to segment, 0-based (null = keep all)
@@ -83,6 +85,8 @@ cellpose:
83
85
  pyramid_levels: 5
84
86
  pyramid_downscale: 2
85
87
  sequential_labels: true # renumber labels to a contiguous 1..N
88
+ shard_labels: false # true → also reshard label level 0 after the
89
+ # merge (one extra pass; see tip below)
86
90
  ```
87
91
 
88
92
  !!! tip "Growing labels after segmentation"
@@ -98,6 +102,77 @@ sequential_labels: true # renumber labels to a contiguous 1..N
98
102
  labels afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
99
103
  for how it works and the equivalent direct-API call.
100
104
 
105
+ !!! tip "What `shard: true` covers, and `shard_labels` for label level 0"
106
+ Without sharding, one chunk is one file, and a fine-chunked level 0 can
107
+ run to ~950k of them — painful on a shared filesystem at write time and
108
+ on every read after. `shard: true` packs chunks into far fewer files
109
+ without changing the chunking or the memory profile.
110
+
111
+ `shard: true` applies to **the converted image (all levels)** and to
112
+ **the label pyramid levels (1..N)**. It cannot apply to **label level
113
+ 0** *while that level is being written*: a shard has to be written whole
114
+ by a single writer, while level 0 is filled one chunk at a time by
115
+ concurrent `segment` jobs (or by the merge's own worker pool). Two
116
+ writers doing a read-modify-write on the same shard file silently lose
117
+ each other's chunks.
118
+
119
+ Once the merge has finished, though, nothing is writing it anymore, so a
120
+ single-threaded pass *can* rewrite it sharded. That is `shard_labels`:
121
+
122
+ ```yaml
123
+ shard: true
124
+ shard_labels: true # or an explicit shape, e.g. [16, 512, 512]
125
+ ```
126
+
127
+ `true` reuses whatever spec `shard` carries; a list overrides it.
128
+ `shard: false` does not veto it — an unsharded image with sharded labels
129
+ is a valid combination. It is opt-in because it costs one extra full
130
+ read+write of level 0, and because it is the level with the most chunks
131
+ it is also the one worth paying for. The array's attributes (including
132
+ the merge's own completion marker) are carried across, so a later rerun
133
+ still sees the merge as done.
134
+
135
+ Check whether the file count is actually a problem for your data first —
136
+ a `(126, 34000, 28500)` image at the `(16, 1024, 1024)` label chunk cap
137
+ is 7,616 files per label group, well under the 200,000 at which the
138
+ conversion starts warning. On scicore that is fine; on a filesystem with
139
+ a tighter inode or per-directory budget it may not be.
140
+
141
+ !!! tip "Which OME-ZARR version gets written (`ngff_version`)"
142
+ OME-ZARR is versioned, and the version decides the **zarr format** as
143
+ well as the metadata layout — the two are not separate choices. NGFF 0.4
144
+ is specified over zarr v2 and puts `multiscales`, `labels` and
145
+ `image-label` at the top level of the store's attributes; 0.5 is the
146
+ zarr-v3 revision and nests them under an `ome` key.
147
+
148
+ `ngff_version: "auto"` (the default) follows the installed zarr, which on
149
+ any current environment means **0.5**. Pin `"0.4"` only when a downstream
150
+ tool still cannot read 0.5:
151
+
152
+ ```yaml
153
+ ngff_version: "0.4"
154
+ shard: false # required: zarr v2 has no sharding codec
155
+ ```
156
+
157
+ That is a real trade-off, not a formality — 0.4 means a zarr-v2 store, and
158
+ sharding is a zarr-v3 feature, so `shard`/`shard_labels` stop doing
159
+ anything. The workflow refuses the combination rather than silently
160
+ ignoring it. The store's own root file changes too (`.zgroup` instead of
161
+ `zarr.json`), which the rules account for.
162
+
163
+ Writing into an **existing** store always follows that store's format,
164
+ whatever this key says: the label pyramid is written by `merge`, long
165
+ after `convert` made the image, and a store with v2 arrays and v3
166
+ metadata is one no reader can open.
167
+
168
+ **What about 0.6?** It was released on 14 September 2026 and patchworks
169
+ does not write it. It is not a version bump but a different metadata
170
+ document: RFC-5 replaces a multiscale's `axes` with `coordinateSystems`
171
+ and requires an `input`/`output` pair on every coordinate transformation.
172
+ Nothing reads it yet either — ome-zarr-py, and so napari, still default
173
+ to 0.5. Setting `ngff_version: "0.6"` is rejected with that explanation
174
+ rather than writing a store you could not open.
175
+
101
176
  !!! tip "Dropping objects by size with `min_volume`/`max_volume`"
102
177
  `min_volume: N` drops any object smaller than `N` µm³; `max_volume: N`
103
178
  drops any object larger than `N` µm³ (e.g. several objects merged into
@@ -112,15 +187,60 @@ sequential_labels: true # renumber labels to a contiguous 1..N
112
187
  [Filtering by size after merge](merging.md#filtering-by-size-after-merge)
113
188
  for the equivalent direct-API call.
114
189
 
190
+ !!! warning "`model:` names are version-specific — check which Cellpose you have"
191
+ Cellpose 3 and 4 have **disjoint** model names: v3 has `cyto3`,
192
+ `nuclei`, …; v4 replaced them all with the `cpsam` family (`cpsam`,
193
+ `cpsam_v2`, `cpdino`, …). Neither version *raises* on a name it doesn't
194
+ know — v4 logs a warning and quietly loads its default (`cpsam_v2`)
195
+ instead. A `cyto3` left in a config against a v4 install therefore
196
+ segments every tile with a model you did not choose, and the only
197
+ evidence is one line in the job log.
198
+
199
+ patchworks now rejects an unavailable name in `prepare`, on the cheap
200
+ CPU job, rather than letting it through. Check what you have with
201
+ `python -c "import cellpose; print(cellpose.version)"`, then either pick
202
+ a name that install offers, point `model:` at a custom-trained model's
203
+ path, or install the version you want — the workflow ships `cellpose3`
204
+ and `cellpose4` pixi environments (`pixi install -e cellpose3`) for
205
+ exactly this.
206
+
115
207
  !!! tip "3-D anisotropy is derived automatically"
116
208
  Cellpose's `do_3D` assumes isotropic voxels unless told otherwise —
117
209
  without an `anisotropy`, a real (anisotropic) dataset gets objects
118
- fragmented or distorted across z. `segment` now derives it from
210
+ fragmented or distorted across z. `segment` derives it from
119
211
  `image.zarr`'s own calibration (`z` voxel size ÷ lateral voxel size)
120
212
  whenever `do_3D: true` and `cellpose.anisotropy` isn't set explicitly, so
121
213
  there's usually nothing to configure. Set `anisotropy:` yourself in the
122
214
  `cellpose:` block to override it.
123
215
 
216
+ Note it is not just a hint to the model: Cellpose *resizes* the tile to
217
+ `z × anisotropy` planes before the net runs. `tile_shape: "auto"`
218
+ budgets for that resized tile, so a 2.2× anisotropy buys a
219
+ correspondingly smaller tile rather than an out-of-memory job.
220
+
221
+ !!! warning "The physically-correct anisotropy is not always the best one"
222
+ Upsampling z by the true ratio can produce **ring artifacts** and costs
223
+ runtime proportional to the ratio. A contributor on
224
+ [cellpose#1408](https://github.com/MouseLand/cellpose/pull/1408) reports
225
+ that `anisotropy: 1` avoids the rings entirely and is much faster, at
226
+ the cost of boundaries being off by a few pixels in the top/bottom
227
+ z-planes, where a cell's cross-section changes fastest — and recommends
228
+ pairing it with light z-only flow smoothing:
229
+
230
+ ```yaml
231
+ cellpose:
232
+ do_3D: true
233
+ anisotropy: 1 # overrides the derived value
234
+ flow3D_smooth: [1, 0, 0] # z, y, x -- smooth z only
235
+ ```
236
+
237
+ This is a genuine trade-off, not a strictly better setting, and it is
238
+ worth testing both on your own data rather than taking either on faith.
239
+ Reach for it especially if 3-D results look ringed or fragmented along
240
+ z: that is the symptom this addresses. `flow3D_smooth` accepts a scalar
241
+ on older Cellpose and a `[z, y, x]` list from the version that merged
242
+ that PR onward.
243
+
124
244
  !!! tip "Tile size vs runtime"
125
245
  `tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
126
246
  more (faster) jobs; very large 3-D tiles are slow. Keep `do_3D: false` (2-D
@@ -317,6 +437,7 @@ input: "/data/scan.ims"
317
437
  work_dir: "/scratch/results"
318
438
  tile_shape: [16, 1024, 1024]
319
439
  shard: false # true → far fewer files, same chunks
440
+ ngff_version: "auto" # convert reads it, so it belongs here
320
441
  tiles_per_job: 4
321
442
  ```
322
443
 
@@ -402,11 +523,21 @@ snakemake --workflow-profile profile/slurm --configfile config/common.yaml confi
402
523
  ```
403
524
 
404
525
  !!! warning "Conversion settings belong in the shared file"
405
- `convert` runs **once**, from the first config only. A `shard`, `input` or
406
- `pyramid_levels` set on the second config is therefore never read, and
407
- nothing logs that it was dropped. `run_multi` refuses to start when those
408
- keys disagree across configs and tells you which one — but if you drive
409
- the configs by hand, keep them in `common.yaml`.
526
+ `convert` runs **once**, from the first config only. A `shard`, `input`,
527
+ `convert_chunks`, `sequence_pattern` or `reuse_pyramid` set on the second
528
+ config is therefore never read, and nothing logs that it was dropped.
529
+ `run_multi` refuses to start when those keys disagree across configs and
530
+ tells you which one — but if you drive the configs by hand, keep them in
531
+ `common.yaml`.
532
+
533
+ `ngff_version` is in that list: it decides the store's zarr format, so a
534
+ second config disagreeing about it would describe a store that is not the
535
+ one on disk.
536
+
537
+ `merge` runs once **per config**, so the keys it reads —
538
+ `pyramid_levels`, `pyramid_downscale`, `sequential_labels`,
539
+ `min_volume`/`max_volume` and `shard_labels` — may legitimately differ
540
+ between them, and are only in `common.yaml` for convenience.
410
541
 
411
542
  Splitting the configs is optional: a self-contained config still works,
412
543
  and `common:` can simply be left out of `multi.yaml`.
@@ -96,7 +96,16 @@ napari = [
96
96
  # openpyxl -> scripts/run_multi.py writes label_relations() output as an
97
97
  # Excel workbook (per-object + per-container sheets), not a plain CSV.
98
98
  workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm", "openpyxl"]
99
- dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
99
+ # jsonschema validates what we write against the vendored official
100
+ # OME-NGFF schemas (tests/ngff_schemas/); without it that one test skips.
101
+ dev = [
102
+ "pytest",
103
+ "pytest-cov",
104
+ "scikit-image",
105
+ "psutil",
106
+ "tqdm",
107
+ "jsonschema>=4.18",
108
+ ]
100
109
  docs = ["mkdocs-material>=9.0", "mkdocstrings[python]>=0.24"]
101
110
  all = [
102
111
  "patchworks[io,gpu,bioio,imaris,napari]",
@@ -380,6 +380,7 @@ def auto_tile_shape_cellpose(
380
380
  model_memory_bytes: int = 2 * 1024**3,
381
381
  cellpose_memory_factor: int = 20,
382
382
  n_channels: int = 1,
383
+ anisotropy: float | None = None,
383
384
  verbose: bool = False,
384
385
  ) -> tuple[int, ...]:
385
386
  """Cellpose-optimised tile shape.
@@ -426,6 +427,13 @@ def auto_tile_shape_cellpose(
426
427
  Channels each tile carries (default 1). Above 1 the per-voxel cost
427
428
  scales with it, so the tile shrinks accordingly -- e.g. the workflow's
428
429
  ``nuclei_channel`` hands Cellpose a cyto+nuclei pair.
430
+ anisotropy:
431
+ The ``anisotropy`` Cellpose will be given (``do_3D`` only). It is not
432
+ merely a hint to the model: Cellpose resizes the tile to
433
+ ``z * anisotropy`` planes before the net runs, so a 2.2x anisotropy
434
+ costs 2.2x the z it was handed. Budgeting against the unscaled ``z``
435
+ under-counts by exactly that factor. ``None`` (the default) assumes
436
+ isotropic, i.e. no resize.
429
437
  verbose:
430
438
  Log the chosen shape and memory estimates.
431
439
 
@@ -441,6 +449,18 @@ def auto_tile_shape_cellpose(
441
449
  (1, 2048, 2048)
442
450
  """
443
451
  n_workers = n_workers or cpu_allocation()
452
+ # Cellpose resizes the tile before the net runs -- by `rescale`
453
+ # (= 30 / diameter) on every axis, and by `anisotropy` on z as well -- so
454
+ # the array it actually holds is bigger than the one it was handed, and a
455
+ # budget computed from the unresized tile under-counts by that factor. A
456
+ # diameter half the model's 30 px means a 2x upsample per axis: 8x the
457
+ # voxels, enough to turn a comfortable tile into an OOM.
458
+ #
459
+ # Both only ever *shrink* the tile. A predicted downsample (diameter > 30)
460
+ # would license a bigger one, but these are a safety margin against a
461
+ # rough memory model, not a measurement to spend headroom on.
462
+ rescale = max(1.0, 30.0 / diameter) if diameter else 1.0
463
+ z_resize = max(1.0, anisotropy or 1.0)
444
464
  # A tile holds n_channels planes per voxel (e.g. Cellpose's
445
465
  # cyto+nuclei pair), so the per-voxel cost -- and every budget
446
466
  # derived from it below -- scales with them.
@@ -467,7 +487,8 @@ def auto_tile_shape_cellpose(
467
487
  min_tile = int(4 * diameter) if diameter is not None else 1
468
488
 
469
489
  if n_spatial == 2 or not do_3D:
470
- max_pixels_2d = max(1, max_raw_bytes // itemsize)
490
+ # Two axes resized, so the cost per configured pixel is rescale**2.
491
+ max_pixels_2d = max(1, int(max_raw_bytes // (itemsize * rescale**2)))
471
492
  tile_side = max(min_tile, int(max_pixels_2d**0.5))
472
493
  if n_spatial == 2:
473
494
  y, x = shape[-2], shape[-1]
@@ -477,7 +498,13 @@ def auto_tile_shape_cellpose(
477
498
  chunk_spatial = [1, min(y, tile_side), min(x, tile_side)]
478
499
  else:
479
500
  z, y, x = shape[-3], shape[-2], shape[-1]
480
- max_pixels_per_slice = max(1, (max_raw_bytes // 3) // (z * itemsize))
501
+ # All three axes are resized: z by anisotropy * rescale, y/x by
502
+ # rescale each -- so one configured voxel costs
503
+ # anisotropy * rescale**3 of them.
504
+ effective_z = z * z_resize * rescale**3
505
+ max_pixels_per_slice = max(
506
+ 1, int((max_raw_bytes // 3) // (effective_z * itemsize))
507
+ )
481
508
  tile_side = max(min_tile, int(max_pixels_per_slice**0.5))
482
509
  chunk_spatial = [z, min(y, tile_side), min(x, tile_side)]
483
510
 
@@ -18,27 +18,38 @@ _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
18
18
  _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
19
19
 
20
20
 
21
- def zarr_compressor_kwargs() -> dict:
21
+ def zarr_compressor_kwargs(zarr_format: int = 3) -> dict:
22
22
  """Keyword arguments pinning the compression codec for a new array.
23
23
 
24
24
  zstd is already zarr v3's default, but relying on a library default means
25
25
  the stores patchworks writes change silently if that default ever moves.
26
26
  Labels in particular are highly compressible, so this is worth stating.
27
27
 
28
+ The codec *object* depends on the format of the array being written, not
29
+ on the installed zarr: zarr-python 3 can write a zarr-v2 array (which is
30
+ what NGFF 0.4 needs), and a v2 array rejects ``zarr.codecs.ZstdCodec`` --
31
+ it wants the numcodecs one.
32
+
33
+ Parameters
34
+ ----------
35
+ zarr_format : int, optional
36
+ Format of the array about to be created, 2 or 3 (default 3).
37
+
28
38
  Returns
29
39
  -------
30
40
  dict
31
- ``compressors=``/``compressor=`` as the installed zarr expects, or
41
+ ``compressors=``/``compressor=`` as that combination expects, or
32
42
  empty if the codec cannot be built (then the default applies).
33
43
  """
34
44
  try:
35
- if _ZARR_V3:
45
+ if _ZARR_V3 and zarr_format != 2:
36
46
  from zarr.codecs import ZstdCodec
37
47
 
38
48
  return {"compressors": (ZstdCodec(level=1),)}
39
49
  import numcodecs
40
50
 
41
- return {"compressor": numcodecs.Zstd(level=1)}
51
+ codec = numcodecs.Zstd(level=1)
52
+ return {"compressors": (codec,)} if _ZARR_V3 else {"compressor": codec}
42
53
  except Exception: # pragma: no cover - depends on the installed zarr
43
54
  logger.debug("could not pin a compressor; using zarr's default")
44
55
  return {}
@@ -253,6 +253,26 @@ def _make_config(
253
253
  }
254
254
 
255
255
 
256
+ def available_models() -> list[str]:
257
+ """Pretrained model names the installed Cellpose actually accepts.
258
+
259
+ The name set changed completely between major versions -- v3 ships
260
+ ``cyto3``/``nuclei``/… , v4 replaced them with ``cpsam``-family names --
261
+ and neither version raises on an unknown one. v4 falls back to its
262
+ default model with only a log line, so a v3 name in a v4 environment
263
+ segments everything with the wrong model and nothing fails. Checking the
264
+ name against this list turns that into a config error instead.
265
+
266
+ Returns
267
+ -------
268
+ list of str
269
+ Model names, or an empty list when Cellpose isn't installed or
270
+ doesn't publish them (in which case no name can be rejected).
271
+ """
272
+ names = getattr(_cellpose_models, "MODEL_NAMES", None)
273
+ return list(names) if names else []
274
+
275
+
256
276
  def _get_model(cellpose_dict: dict[str, Any]) -> Any:
257
277
  """Return a worker-local cached Cellpose model.
258
278
 
@@ -272,8 +292,14 @@ def _get_model(cellpose_dict: dict[str, Any]) -> Any:
272
292
  gpu = cellpose_dict.get("gpu", False)
273
293
  model_type = cellpose_dict["model"]
274
294
  if _CELLPOSE_V4:
295
+ # v4 renamed this: `model_type=` is accepted but explicitly
296
+ # ignored ("not used in v4.0.1+"), leaving pretrained_model at
297
+ # its "cpsam_v2" default -- so passing the configured name there
298
+ # silently segmented *every* config with the same default model,
299
+ # whatever `model:` said, with only a logger warning to show for
300
+ # it. `available_models()` rejects an unusable name up front.
275
301
  _model_cache[key] = _cellpose_models.CellposeModel(
276
- model_type=model_type, gpu=gpu
302
+ pretrained_model=model_type, gpu=gpu
277
303
  )
278
304
  else:
279
305
  _model_cache[key] = _cellpose_models.Cellpose(