patchworks 3.2.0__tar.gz → 3.3.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 (169) hide show
  1. {patchworks-3.2.0 → patchworks-3.3.0}/.github/workflows/test.yml +2 -4
  2. {patchworks-3.2.0 → patchworks-3.3.0}/PKG-INFO +2 -2
  3. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/dog.md +18 -0
  4. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/cli.md +10 -0
  5. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/membrane_cells.md +32 -2
  6. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/review.md +7 -3
  7. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/snakemake.md +49 -18
  8. {patchworks-3.2.0 → patchworks-3.3.0}/pyproject.toml +2 -1
  9. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_distributed.py +40 -2
  10. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_merge.py +253 -18
  11. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_occupancy.py +1 -1
  12. patchworks-3.3.0/src/patchworks/_relations.py +329 -0
  13. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_review.py +43 -7
  14. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_tables.py +187 -36
  15. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_volume_filter.py +44 -8
  16. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/cli.py +26 -1
  17. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/dog.py +39 -1
  18. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/ome_zarr.py +176 -8
  19. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/plantseg.py +282 -20
  20. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_distributed.py +163 -0
  21. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_dog.py +17 -0
  22. patchworks-3.3.0/tests/test_ngff_conformance.py +177 -0
  23. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_ome_zarr.py +4 -3
  24. patchworks-3.3.0/tests/test_plantseg.py +369 -0
  25. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_position.py +90 -7
  26. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_pw.py +144 -2
  27. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_relations.py +59 -1
  28. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_run_multi.py +614 -27
  29. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_volume_filter.py +35 -0
  30. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/config.yaml +10 -2
  31. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/config_cilia.yaml +5 -0
  32. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/multi.yaml +5 -2
  33. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/pixi.toml +53 -17
  34. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/rules/common.smk +3 -1
  35. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/_pw.py +180 -2
  36. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/export_iso.py +35 -3
  37. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/merge.py +85 -28
  38. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/prepare_tiles.py +12 -1
  39. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/relate.py +124 -65
  40. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/run_multi.py +455 -46
  41. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/segment_tile.py +9 -0
  42. patchworks-3.2.0/src/patchworks/_relations.py +0 -194
  43. patchworks-3.2.0/tests/test_plantseg.py +0 -175
  44. {patchworks-3.2.0 → patchworks-3.3.0}/.github/min-versions.txt +0 -0
  45. {patchworks-3.2.0 → patchworks-3.3.0}/.github/workflows/docs.yml +0 -0
  46. {patchworks-3.2.0 → patchworks-3.3.0}/.github/workflows/lint.yml +0 -0
  47. {patchworks-3.2.0 → patchworks-3.3.0}/.github/workflows/release.yml +0 -0
  48. {patchworks-3.2.0 → patchworks-3.3.0}/.gitignore +0 -0
  49. {patchworks-3.2.0 → patchworks-3.3.0}/.markdownlint-cli2.yaml +0 -0
  50. {patchworks-3.2.0 → patchworks-3.3.0}/.pre-commit-config.yaml +0 -0
  51. {patchworks-3.2.0 → patchworks-3.3.0}/LICENSE +0 -0
  52. {patchworks-3.2.0 → patchworks-3.3.0}/README.md +0 -0
  53. {patchworks-3.2.0 → patchworks-3.3.0}/benchmarks/bench.py +0 -0
  54. {patchworks-3.2.0 → patchworks-3.3.0}/benchmarks/compare.py +0 -0
  55. {patchworks-3.2.0 → patchworks-3.3.0}/cliff.toml +0 -0
  56. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/chunks.md +0 -0
  57. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/cluster.md +0 -0
  58. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/io.md +0 -0
  59. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/merge_tile_labels.md +0 -0
  60. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/careamics.md +0 -0
  61. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/cellpose.md +0 -0
  62. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/dog.md +0 -0
  63. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/napari.md +0 -0
  64. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/ome_zarr.md +0 -0
  65. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/plantseg.md +0 -0
  66. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/plugins/watershed.md +0 -0
  67. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/postprocess.md +0 -0
  68. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/provenance.md +0 -0
  69. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/relabel.md +0 -0
  70. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/review.md +0 -0
  71. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/seams.md +0 -0
  72. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/tile_process.md +0 -0
  73. {patchworks-3.2.0 → patchworks-3.3.0}/docs/api/volume_filter.md +0 -0
  74. {patchworks-3.2.0 → patchworks-3.3.0}/docs/assets/logo.png +0 -0
  75. {patchworks-3.2.0 → patchworks-3.3.0}/docs/assets/review_panel.png +0 -0
  76. {patchworks-3.2.0 → patchworks-3.3.0}/docs/assets/review_position.png +0 -0
  77. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/cellpose_2d.md +0 -0
  78. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/cellpose_2d.py +0 -0
  79. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/cellpose_3d.md +0 -0
  80. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/cellpose_3d.py +0 -0
  81. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/custom.md +0 -0
  82. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/custom_method.py +0 -0
  83. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/dog.py +0 -0
  84. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/standalone_merge.md +0 -0
  85. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/stardist.md +0 -0
  86. {patchworks-3.2.0 → patchworks-3.3.0}/docs/examples/stardist_2d.py +0 -0
  87. {patchworks-3.2.0 → patchworks-3.3.0}/docs/getting_started.md +0 -0
  88. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/custom_segmentation.md +0 -0
  89. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/gpu_distributed.md +0 -0
  90. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/label_relations.md +0 -0
  91. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/launcher.md +0 -0
  92. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/measurements.md +0 -0
  93. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/merging.md +0 -0
  94. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/ome_zarr_napari.md +0 -0
  95. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/performance.md +0 -0
  96. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/pitfalls.md +0 -0
  97. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/skip_empty.md +0 -0
  98. {patchworks-3.2.0 → patchworks-3.3.0}/docs/guide/tiling.md +0 -0
  99. {patchworks-3.2.0 → patchworks-3.3.0}/docs/index.md +0 -0
  100. {patchworks-3.2.0 → patchworks-3.3.0}/mkdocs.yml +0 -0
  101. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/__init__.py +0 -0
  102. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_autotune.py +0 -0
  103. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_chunks.py +0 -0
  104. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_cluster.py +0 -0
  105. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_core.py +0 -0
  106. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_gpu.py +0 -0
  107. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_io.py +0 -0
  108. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_notify.py +0 -0
  109. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_postprocess.py +0 -0
  110. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_progress.py +0 -0
  111. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_provenance.py +0 -0
  112. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_relabel.py +0 -0
  113. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/_seams.py +0 -0
  114. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/__init__.py +0 -0
  115. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/careamics.py +0 -0
  116. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/cellpose.py +0 -0
  117. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/napari.py +0 -0
  118. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/review.py +0 -0
  119. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/plugins/watershed.py +0 -0
  120. {patchworks-3.2.0 → patchworks-3.3.0}/src/patchworks/py.typed +0 -0
  121. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/image.schema +0 -0
  122. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/label.schema +0 -0
  123. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.4/ome.schema +0 -0
  124. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/_version.schema +0 -0
  125. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/image.schema +0 -0
  126. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/label.schema +0 -0
  127. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/0.5/ome.schema +0 -0
  128. {patchworks-3.2.0 → patchworks-3.3.0}/tests/ngff_schemas/README.md +0 -0
  129. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_allocation.py +0 -0
  130. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_autotune.py +0 -0
  131. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_careamics.py +0 -0
  132. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_cellpose.py +0 -0
  133. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_cli.py +0 -0
  134. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_core.py +0 -0
  135. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_gpu.py +0 -0
  136. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_launcher.py +0 -0
  137. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_napari.py +0 -0
  138. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_notify.py +0 -0
  139. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_occupancy.py +0 -0
  140. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_postprocess.py +0 -0
  141. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_progress.py +0 -0
  142. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_remote.py +0 -0
  143. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_review.py +0 -0
  144. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_review_napari.py +0 -0
  145. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_seams.py +0 -0
  146. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_tables.py +0 -0
  147. {patchworks-3.2.0 → patchworks-3.3.0}/tests/test_watershed.py +0 -0
  148. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/README.md +0 -0
  149. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/Snakefile +0 -0
  150. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/common.yaml +0 -0
  151. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/config_cyto.yaml +0 -0
  152. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/config_cyto_plantseg.yaml +0 -0
  153. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/config_nuclei.yaml +0 -0
  154. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/config/multi_plantseg.yaml +0 -0
  155. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/launcher/README.md +0 -0
  156. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/launcher/app.py +0 -0
  157. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/launcher/clusters.yaml +0 -0
  158. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/launcher/launcher_core.py +0 -0
  159. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/launcher/requirements.txt +0 -0
  160. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/profile/slurm/config.yaml +0 -0
  161. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/rules/convert.smk +0 -0
  162. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/rules/merge.smk +0 -0
  163. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/rules/segment.smk +0 -0
  164. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/build_occupancy.py +0 -0
  165. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/convert.py +0 -0
  166. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/fetch_model.py +0 -0
  167. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/reshard_store.py +0 -0
  168. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/scripts/view.py +0 -0
  169. {patchworks-3.2.0 → patchworks-3.3.0}/workflow/viewer/pixi.toml +0 -0
@@ -32,11 +32,9 @@ jobs:
32
32
  run: pytest -q --doctest-modules src
33
33
 
34
34
  other-platforms:
35
- # Not yet verified on macOS or Windows (no CI ran there before), so this
36
- # job reports without failing the build; promote it into the matrix
37
- # above once it is green.
35
+ # Green on macOS and Windows since 2026-10-07: a failure there now fails
36
+ # the build like any other.
38
37
  runs-on: ${{ matrix.os }}
39
- continue-on-error: true
40
38
  strategy:
41
39
  fail-fast: false
42
40
  matrix:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 3.2.0
3
+ Version: 3.3.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
@@ -102,7 +102,7 @@ Provides-Extra: workflow
102
102
  Requires-Dist: openpyxl; extra == 'workflow'
103
103
  Requires-Dist: pandas>=2.0; extra == 'workflow'
104
104
  Requires-Dist: scikit-image; extra == 'workflow'
105
- Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
105
+ Requires-Dist: snakemake-executor-plugin-slurm>=2.2; extra == 'workflow'
106
106
  Requires-Dist: snakemake>=8; extra == 'workflow'
107
107
  Description-Content-Type: text/markdown
108
108
 
@@ -54,6 +54,24 @@ directly to the DoG image — start near the DoG's typical peak value on a
54
54
  known-positive region and adjust from there; there's no auto (Otsu-style)
55
55
  option, since the DoG image isn't bimodal the way a raw intensity image is.
56
56
 
57
+ ## Thin, oblique objects: `connectivity`
58
+
59
+ The thresholded voxels are joined into objects across shared **faces** by
60
+ default. A cilium lying obliquely is a staircase of voxels touching only
61
+ along edges or at corners, and comes out as a row of fragments.
62
+ `connectivity=2` also joins voxels sharing an edge, `connectivity=3` (3-D)
63
+ also a corner:
64
+
65
+ ```python
66
+ fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02, connectivity=3)
67
+ ```
68
+
69
+ The merge has to join tiles the same way, or objects crossing a tile
70
+ boundary diagonally are split there: the workflow does this by itself; with
71
+ the API, pass the same value to `merge_tile_labels(..., connectivity=3)`.
72
+ The merged result is then exactly that of labelling the whole image at once,
73
+ whatever the tile size.
74
+
57
75
  ## GPU
58
76
 
59
77
  ```python
@@ -25,8 +25,18 @@ patchworks tables scan.zarr --relate nuclei:cells
25
25
 
26
26
  # Look at the likely mistakes one by one and fix them (napari)
27
27
  patchworks review scan.zarr --expect cells:nuclei=1
28
+
29
+ # Bring a store written by an older patchworks up to the OME-Zarr spec
30
+ patchworks fix-metadata scan.zarr
28
31
  ```
29
32
 
33
+ `patchworks fix-metadata` changes metadata only, in place: it gives every
34
+ OME-Zarr 0.5 array its `dimension_names` (required by 0.5; versions before
35
+ this one wrote none, which strict readers such as `ome-zarr-models` refuse),
36
+ and drops surplus coarse levels from a label image with more pyramid levels
37
+ than its image (the spec requires the same number). Running it twice
38
+ changes nothing the second time.
39
+
30
40
  `patchworks review` without a window: `--summary` (counts and error
31
41
  estimate), `--export DIR --format csv|xlsx|parquet` (corrected tables),
32
42
  `--workbooks DIR` (relation workbooks), `--write-labels NAME` (a label image
@@ -68,6 +68,7 @@ gets closed by the partitioning, where Cellpose would merge across it.
68
68
  PlantSeg is on conda-forge only, so it has its own pixi environment:
69
69
 
70
70
  ```bash
71
+ export CONDA_OVERRIDE_CUDA=12.0 # login node without a GPU: see below
71
72
  pixi install -e plantseg
72
73
  pixi run -e plantseg plantseg-fetch generic_confocal_3D_unet # once, with internet
73
74
  pixi run -e plantseg multi-slurm
@@ -162,8 +163,37 @@ pixi run -e plantseg multi-plantseg-dry # check the plan
162
163
  pixi run -e plantseg multi-plantseg-slurm # submit
163
164
  ```
164
165
 
165
- The `plantseg` environment is the default one plus PlantSeg, so the
166
- Cellpose run comes from it too. Afterwards, `nuclei_to_cyto.xlsx` gives
166
+ Your own multi config with a PlantSeg segmentation in it runs the same
167
+ way, from the same environment:
168
+
169
+ ```bash
170
+ pixi run -e plantseg multi-slurm --config /path/to/my_multi.yaml
171
+ ```
172
+
173
+ !!! note "`Virtual package '__cuda' does not match` on a login node"
174
+
175
+ The `plantseg` environment holds a CUDA build of PyTorch, and pixi
176
+ refuses to install or run an environment needing CUDA on a machine with
177
+ no GPU driver -- a login node. Only the GPU jobs need CUDA, and they
178
+ start the environment's Python directly, without pixi. Tell pixi on the
179
+ login node that a driver is there:
180
+
181
+ ```bash
182
+ export CONDA_OVERRIDE_CUDA=12.0 # any 12.x; put it in ~/.bashrc
183
+ ```
184
+
185
+ It must be set for every `pixi run -e plantseg ...` there, not only for
186
+ the install. Check the GPU nodes' driver supports CUDA 12 (`nvidia-smi`
187
+ in a GPU job prints "CUDA Version: 12.x" or later).
188
+
189
+ The `-e plantseg` matters: every job runs in the environment the command
190
+ was started from, and the default one has no PlantSeg. `run_multi` checks
191
+ this before converting anything and says which environment to use (as it
192
+ does for `denoise:`, which needs `-e careamics`).
193
+
194
+ The `plantseg` environment is the default one plus PlantSeg and cupy, so
195
+ the Cellpose run comes from it too, as does a cilia config with the DoG
196
+ plugin's `use_gpu: true` (cupy) and deconvolution. Afterwards, `nuclei_to_cyto.xlsx` gives
167
197
  each nucleus its cell, and `pixi run -e viewer review <work_dir>/image.zarr`
168
198
  lists any cell not holding exactly one nucleus.
169
199
 
@@ -223,9 +223,13 @@ image.zarr/labels/cilia_labels/
223
223
  So it travels with the labels, including in a zip bundle, and it is
224
224
  replaced whenever the labels are. A table computed from older labels is
225
225
  recognised and ignored, never shown against the wrong segmentation.
226
- Measuring costs one read of the labels, after the merge. Add intensity
227
- columns with `table_channels: [0, 2]`, or turn tables off with
228
- `object_table: false` (see the [workflow config](snakemake.md)).
226
+ The table costs no extra read of the labels: each segment job measures
227
+ its tiles' objects as it writes them, and the merge adds those sums up per
228
+ merged object (an object cut by tile boundaries gets exactly the values it
229
+ would have measured whole). Intensity columns (`table_channels: [0, 2]`)
230
+ need the image, so with them the merged labels are measured once instead.
231
+ Turn tables off with `object_table: false` (see the
232
+ [workflow config](snakemake.md)).
229
233
 
230
234
  For a store that has none, for example a run made before tables existed,
231
235
  or labels from elsewhere:
@@ -363,8 +363,12 @@ shard_labels: false # true → also reshard label level 0 after the
363
363
  one blob). Set either, both, or neither (`null`, the default, disables
364
364
  each). Both run once on the **fully merged** image — not per tile, where
365
365
  an object crossing a tile boundary would look smaller or larger than it
366
- really is. Runs after `merge` and before the pyramid is built, so every
367
- pyramid level reflects the filtered result, and needs `image.zarr` to
366
+ really is. The sizes come from what the segment jobs measured per tile,
367
+ added up per merged object, and the filter is applied in the merge's
368
+ own relabelling pass, so it costs no pass of its own over the image
369
+ (runs whose tiles were segmented by an older version filter the merged
370
+ labels in a separate pass instead). Every pyramid level reflects the
371
+ filtered result. It needs `image.zarr` to
368
372
  carry a pixel size (the same calibration deconvolution's voxel sizes and
369
373
  Cellpose's `anisotropy` are derived from — see the tip below); an
370
374
  uncalibrated store raises rather than silently skipping the filter. See
@@ -650,6 +654,14 @@ running the workflow **twice with two configs against the same `work_dir`**
650
654
  never collides: each run gets its own private subdirectory, and both reuse
651
655
  the *same* already-converted `image.zarr` (conversion never re-runs).
652
656
 
657
+ `labels.done` is what makes Snakemake call a segmentation finished, while
658
+ the labels themselves are in `image.zarr/labels/<label_name>`. Deleting only
659
+ the latter leaves the run "done" with nothing re-making it: `run_multi`
660
+ refuses that up front and names the `work_dir/<label_name>` folder to remove
661
+ for a fresh segmentation. It also refuses a relation naming a label image
662
+ that no listed config makes and the store does not hold, and skips (with a
663
+ message) a relation whose labels are still missing once everything has run.
664
+
653
665
  Most of what those configs contain is identical — the input, the `work_dir`,
654
666
  the tiling, everything `convert` reads. Put it in **one** shared file and let
655
667
  each config carry only what actually differs. Snakemake merges several
@@ -810,9 +822,10 @@ results/image.zarr/labels/cyto_labels/
810
822
  rule, so it doesn't get a `log:` directive for free. Standalone (or
811
823
  under plain `multi`), it writes to `<work_dir>/logs/relate.log`
812
824
  (override with `relate.py --log`), the same tee-to-file-and-stdout
813
- behaviour as the other steps. Under `multi-slurm`, where each pair is
814
- its own concurrent job, `run_multi.py` points each one at its own file
815
- instead — `<work_dir>/logs/relate/<a>_to_<b>.log` — so concurrent pairs
825
+ behaviour as the other steps. Under `multi-slurm`, where each parent's
826
+ relations are one concurrent job, `run_multi.py` points each job at its
827
+ own file instead — `<work_dir>/logs/relate/to_<parent>.log`, or
828
+ `<a>_to_<b>.log` for a parent with a single child — so concurrent jobs
816
829
  don't interleave into one log; check there instead of scrolling back
817
830
  through `srun`'s live output.
818
831
 
@@ -856,6 +869,19 @@ pixi run multi # run locally
856
869
  pixi run multi-slurm # submit every segmentation to SLURM
857
870
  ```
858
871
 
872
+ They read `config/multi.yaml` unless given another with `--config`:
873
+
874
+ ```bash
875
+ pixi run multi-slurm --config /data/run42/multi.yaml
876
+ pixi run multi-dry --config my_multi.yaml # relative to where you run pixi
877
+ ```
878
+
879
+ A relative `--config` is looked for in the directory you run `pixi` from,
880
+ then in `workflow/`. The `common:` and `segmentations:` paths inside it are
881
+ looked for next to the multi config first, then in `workflow/` -- so a run's
882
+ configs can live together in a folder of their own, beside the data, while
883
+ the shipped `config/multi.yaml` keeps working as it is.
884
+
859
885
  Before anything is submitted, the script checks that every listed config
860
886
  shares one `work_dir` (so `label_relations` has a single `image.zarr` to read
861
887
  both label groups from), that `tile_shape` and `level` are identical (so the
@@ -893,7 +919,7 @@ abort the others; you get a per-config status and a non-zero exit.
893
919
  # config/multi.yaml
894
920
  relate:
895
921
  qos: "1day"
896
- time: 720 # minutes, per pair; must stay under that QOS's MaxWall
922
+ time: 720 # minutes, per job (one per parent); under the QOS's MaxWall
897
923
  ```
898
924
 
899
925
  A `--relate-*` flag overrides the block for that one key; anything the
@@ -902,18 +928,23 @@ abort the others; you get a per-config status and a non-zero exit.
902
928
  default you meant to replace. Under plain `multi` (no `--profile`), relations
903
929
  still run locally, in-process, one after another, as before.
904
930
 
905
- Each pair logs its shape, chunk count and object count before it starts,
906
- then a progress line roughly once a minute (`label_relations: 412/3,600
907
- (11%) after 7m, ~55m left`), so a long relation is distinguishable from a
908
- hung one in `logs/relate/<a>_to_<b>.log`.
909
-
910
- Because every pair gets its own job, one running long no longer starves
911
- the others out of a shared time budget, and a pair that gets killed no
912
- longer takes an already-finished sibling's workbook down with it.
913
- `relate.py` also skips a pair whose `.xlsx` is already newer than both
914
- labels' merge marker, so **re-running the exact same `multi-slurm`
915
- command only recomputes what's still missing or stale** — delete a
916
- specific `.xlsx` yourself to force just that one to recompute.
931
+ The relations are grouped by parent: one job reads each parent label
932
+ image once for all its children (e.g. nuclei, cilia and the other cell
933
+ segmentation against `cyto_labels`), and only the chunks some child has
934
+ labels in. Each job logs its images' shape, chunking and object counts
935
+ before it starts, then a progress line roughly once a minute
936
+ (`label_relations: 412/3,600 (11%) after 7m, ~55m left`), so a long
937
+ relation is distinguishable from a hung one in `logs/relate/`.
938
+
939
+ Because each parent gets its own job, one running long does not starve
940
+ the others out of a shared time budget. `relate.py` skips a pair whose
941
+ workbook (the `.xlsx`, or the two `.csv` files a sheet too long for
942
+ Excel is written as) is already newer than both labels' merge marker,
943
+ and rewrites a missing workbook from the object tables when they
944
+ already hold the relation for the current labels, so **re-running the
945
+ exact same `multi-slurm` command only recomputes what's still missing
946
+ or stale** — delete a specific workbook to force just that one. The
947
+ bundle is likewise left alone when nothing in the store changed.
917
948
 
918
949
  !!! tip "After a killed run"
919
950
  Snakemake only releases its lock on a clean exit, so a run that was killed
@@ -119,7 +119,8 @@ napari = [
119
119
  # workbook (per-object + per-container sheets) from the object tables.
120
120
  workflow = [
121
121
  "snakemake>=8",
122
- "snakemake-executor-plugin-slurm",
122
+ # >=2.2: profile/slurm sets slurm-jobname-prefix, unknown before 2.2
123
+ "snakemake-executor-plugin-slurm>=2.2",
123
124
  "openpyxl",
124
125
  "pandas>=2.0",
125
126
  # method: "threshold" and the nuclei-seeded watershed plugin
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import itertools
15
15
  import logging
16
+ import os
16
17
  from pathlib import Path
17
18
  from typing import Callable, Sequence, Union
18
19
 
@@ -124,6 +125,7 @@ def create_stage(
124
125
  *,
125
126
  component: str = "staged",
126
127
  dtype=np.int32,
128
+ zarr_format: int | None = None,
127
129
  ) -> str:
128
130
  """Create the empty (zero-filled) shared stage store for tiled writes.
129
131
 
@@ -140,19 +142,27 @@ def create_stage(
140
142
  dtype : data-type, optional
141
143
  Label dtype (default ``int32``). Tiles write local labels; the merge's
142
144
  first pass renumbers them to a compact global range that fits int32.
145
+ zarr_format : int, optional
146
+ 2 or 3. Give the image store's own format when staging straight into
147
+ its ``labels/<name>`` group: a v3 group inside a v2 (OME-Zarr 0.4)
148
+ store is something no reader can open. Default: zarr's.
143
149
 
144
150
  Returns
145
151
  -------
146
152
  str
147
153
  The stage store path.
148
154
  """
149
- root = zarr.open_group(str(stage_path), mode="w")
155
+ root = zarr.open_group(
156
+ str(stage_path),
157
+ mode="w",
158
+ **({"zarr_format": zarr_format} if zarr_format else {}),
159
+ )
150
160
  root.create_array(
151
161
  name=component,
152
162
  shape=shape,
153
163
  chunks=tile_shape,
154
164
  dtype=dtype,
155
- **zarr_compressor_kwargs(),
165
+ **zarr_compressor_kwargs(root.metadata.zarr_format),
156
166
  )
157
167
  return str(stage_path)
158
168
 
@@ -168,6 +178,7 @@ def stage_tile(
168
178
  component: str = "staged",
169
179
  channel_axis: int | None = None,
170
180
  halo_dir: Union[str, Path, None] = None,
181
+ parts_dir: Union[str, Path, None] = None,
171
182
  ) -> int:
172
183
  """Run *fn* on a single tile and write it into the shared stage store.
173
184
 
@@ -208,6 +219,13 @@ def stage_tile(
208
219
  (keys ``"<axis>+"``/``"<axis>-"``), labelled with the same ``1..n``
209
220
  ids as the staged core. An object seen only in the halo is 0 there:
210
221
  it has no id in this tile. ``None`` (default) keeps nothing.
222
+ parts_dir : str or Path, optional
223
+ Also measure the tile's objects as written (size, centroid, spread,
224
+ bounding box; :func:`patchworks._tables.tile_partial`) into
225
+ ``<parts_dir>/<index>.npz``, keyed by the staged ``1..n`` ids. The
226
+ merge combines them into the object table without reading the
227
+ labels again (``merge_tile_labels(..., parts_dir=...)``). A tile
228
+ without labels writes none.
211
229
 
212
230
  Returns
213
231
  -------
@@ -272,9 +290,29 @@ def stage_tile(
272
290
  n_labels = int(trimmed.max())
273
291
  dst = zarr.open_group(str(stage_path), mode="r+")[component]
274
292
  dst[sl] = trimmed.astype(dst.dtype)
293
+ if parts_dir is not None and n_labels:
294
+ _save_partial(trimmed, sl, parts_dir, index)
275
295
  return n_labels
276
296
 
277
297
 
298
+ def _save_partial(trimmed, sl, parts_dir, index) -> None:
299
+ """Write one tile's per-object sums to ``<parts_dir>/<index>.npz``.
300
+
301
+ Through a temporary name and a rename, so a job killed mid-write leaves
302
+ no truncated file for the merge to trust.
303
+ """
304
+ from ._tables import tile_partial
305
+
306
+ part = tile_partial(trimmed, [s.start for s in sl])
307
+ if part is None:
308
+ return
309
+ parts_dir = Path(parts_dir)
310
+ parts_dir.mkdir(parents=True, exist_ok=True)
311
+ tmp = parts_dir / f".{int(index)}.tmp.npz"
312
+ np.savez(tmp, **part)
313
+ os.replace(tmp, parts_dir / f"{int(index)}.npz")
314
+
315
+
278
316
  def _core_lut(core: np.ndarray, max_id: int) -> np.ndarray:
279
317
  """LUT renumbering *core*'s ids to ``1..n`` (as ``relabel_sequential``);
280
318
  ids absent from the core map to 0."""