patchworks 2.6.5__tar.gz → 2.6.6__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 (102) hide show
  1. {patchworks-2.6.5 → patchworks-2.6.6}/PKG-INFO +1 -1
  2. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/label_relations.md +15 -1
  3. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/snakemake.md +14 -12
  4. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_run_multi.py +60 -0
  5. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/relate.py +35 -0
  6. {patchworks-2.6.5 → patchworks-2.6.6}/.github/workflows/docs.yml +0 -0
  7. {patchworks-2.6.5 → patchworks-2.6.6}/.github/workflows/lint.yml +0 -0
  8. {patchworks-2.6.5 → patchworks-2.6.6}/.github/workflows/release.yml +0 -0
  9. {patchworks-2.6.5 → patchworks-2.6.6}/.gitignore +0 -0
  10. {patchworks-2.6.5 → patchworks-2.6.6}/.markdownlint-cli2.yaml +0 -0
  11. {patchworks-2.6.5 → patchworks-2.6.6}/LICENSE +0 -0
  12. {patchworks-2.6.5 → patchworks-2.6.6}/README.md +0 -0
  13. {patchworks-2.6.5 → patchworks-2.6.6}/cliff.toml +0 -0
  14. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/chunks.md +0 -0
  15. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/cluster.md +0 -0
  16. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/io.md +0 -0
  17. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/merge_tile_labels.md +0 -0
  18. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/plugins/cellpose.md +0 -0
  19. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/plugins/dog.md +0 -0
  20. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/plugins/napari.md +0 -0
  21. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/plugins/ome_zarr.md +0 -0
  22. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/postprocess.md +0 -0
  23. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/relabel.md +0 -0
  24. {patchworks-2.6.5 → patchworks-2.6.6}/docs/api/tile_process.md +0 -0
  25. {patchworks-2.6.5 → patchworks-2.6.6}/docs/assets/logo.png +0 -0
  26. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/cellpose_2d.md +0 -0
  27. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/cellpose_2d.py +0 -0
  28. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/cellpose_3d.md +0 -0
  29. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/cellpose_3d.py +0 -0
  30. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/custom.md +0 -0
  31. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/custom_method.py +0 -0
  32. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/dog.md +0 -0
  33. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/dog.py +0 -0
  34. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/standalone_merge.md +0 -0
  35. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/stardist.md +0 -0
  36. {patchworks-2.6.5 → patchworks-2.6.6}/docs/examples/stardist_2d.py +0 -0
  37. {patchworks-2.6.5 → patchworks-2.6.6}/docs/getting_started.md +0 -0
  38. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/custom_segmentation.md +0 -0
  39. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/gpu_distributed.md +0 -0
  40. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/measurements.md +0 -0
  41. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/merging.md +0 -0
  42. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/ome_zarr_napari.md +0 -0
  43. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/performance.md +0 -0
  44. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/pitfalls.md +0 -0
  45. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/skip_empty.md +0 -0
  46. {patchworks-2.6.5 → patchworks-2.6.6}/docs/guide/tiling.md +0 -0
  47. {patchworks-2.6.5 → patchworks-2.6.6}/docs/index.md +0 -0
  48. {patchworks-2.6.5 → patchworks-2.6.6}/mkdocs.yml +0 -0
  49. {patchworks-2.6.5 → patchworks-2.6.6}/pyproject.toml +0 -0
  50. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/__init__.py +0 -0
  51. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_chunks.py +0 -0
  52. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_cluster.py +0 -0
  53. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_core.py +0 -0
  54. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_distributed.py +0 -0
  55. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_gpu.py +0 -0
  56. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_io.py +0 -0
  57. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_merge.py +0 -0
  58. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_notify.py +0 -0
  59. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_occupancy.py +0 -0
  60. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_postprocess.py +0 -0
  61. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_progress.py +0 -0
  62. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_relabel.py +0 -0
  63. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/_relations.py +0 -0
  64. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/plugins/__init__.py +0 -0
  65. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/plugins/cellpose.py +0 -0
  66. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/plugins/dog.py +0 -0
  67. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/plugins/napari.py +0 -0
  68. {patchworks-2.6.5 → patchworks-2.6.6}/src/patchworks/plugins/ome_zarr.py +0 -0
  69. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_allocation.py +0 -0
  70. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_core.py +0 -0
  71. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_distributed.py +0 -0
  72. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_dog.py +0 -0
  73. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_gpu.py +0 -0
  74. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_napari.py +0 -0
  75. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_notify.py +0 -0
  76. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_occupancy.py +0 -0
  77. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_ome_zarr.py +0 -0
  78. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_postprocess.py +0 -0
  79. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_progress.py +0 -0
  80. {patchworks-2.6.5 → patchworks-2.6.6}/tests/test_relations.py +0 -0
  81. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/README.md +0 -0
  82. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/Snakefile +0 -0
  83. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/common.yaml +0 -0
  84. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/config.yaml +0 -0
  85. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/config_cilia.yaml +0 -0
  86. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/config_cyto.yaml +0 -0
  87. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/config_nuclei.yaml +0 -0
  88. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/config/multi.yaml +0 -0
  89. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/pixi.toml +0 -0
  90. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/profile/slurm/config.yaml +0 -0
  91. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/rules/common.smk +0 -0
  92. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/rules/convert.smk +0 -0
  93. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/rules/merge.smk +0 -0
  94. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/rules/segment.smk +0 -0
  95. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/_pw.py +0 -0
  96. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/build_occupancy.py +0 -0
  97. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/convert.py +0 -0
  98. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/fetch_model.py +0 -0
  99. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/merge.py +0 -0
  100. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/prepare_tiles.py +0 -0
  101. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/run_multi.py +0 -0
  102. {patchworks-2.6.5 → patchworks-2.6.6}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 2.6.5
3
+ Version: 2.6.6
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
@@ -6,7 +6,21 @@ the other — by streaming both arrays chunk by chunk, so it scales to
6
6
  hundreds of thousands of objects without loading anything fully into RAM.
7
7
 
8
8
  Both label arrays must share the exact same chunk layout — same
9
- `tile_shape`/pyramid `level` when they were produced.
9
+ `tile_shape`/pyramid `level` when they were produced. `label_relations()`
10
+ raises rather than silently doing something slower if they don't; rechunk
11
+ one side to match first (same shape, different chunking is a normal dask
12
+ op — some extra I/O reading across misaligned source chunks, not a
13
+ correctness issue):
14
+
15
+ ```python
16
+ if nuclei.chunks != cells.chunks:
17
+ cells = cells.rechunk(nuclei.chunks)
18
+ ```
19
+
20
+ (The cluster workflow's `run_multi.py`/`relate.py` does this automatically
21
+ — see below — so two configs segmented at different `tile_shape` still
22
+ relate correctly; you only need to do it by hand when calling
23
+ `label_relations()` directly.)
10
24
 
11
25
  ```python
12
26
  import dask.array as da
@@ -398,20 +398,22 @@ results/image.zarr/labels/nuclei_labels/
398
398
  results/image.zarr/labels/cyto_labels/
399
399
  ```
400
400
 
401
- !!! tip "Keep `level` identical across configs; `tile_shape` usually takes care of itself"
401
+ !!! tip "Keep `level` identical across configs; `tile_shape` doesn't have to match anymore"
402
402
  Different segmentations of the same image can use different `channel`
403
403
  and `cellpose:` settings freely, but keep `level` the same across
404
- configs — the label arrays then share the exact same chunk layout, which
405
- [`label_relations()`](label_relations.md) requires.
406
-
407
- `tile_shape` is a little more forgiving under `run_multi.py`: leaving it
408
- on `"auto"` (the default) would normally size a `nuclei_channel` config's
409
- tile smaller than a single-channel one (the sizer charges per channel),
410
- breaking that same requirement — `run_multi.py` detects this and pins
411
- every config to the smallest computed tile automatically, so mixed
412
- single-/two-channel configs under `"auto"` just work. Setting an
413
- explicit `tile_shape` yourself still requires it to be identical across
414
- configs, same as before.
404
+ configs so the label arrays cover the same voxel grid.
405
+
406
+ `tile_shape` itself no longer has to match. Two configs naturally
407
+ produce different on-disk chunk layouts when `tile_shape: "auto"`
408
+ charges a `nuclei_channel` config for a smaller tile than a
409
+ single-channel one, or when one config was segmented before the other's
410
+ settings changed, or a cheaper method (e.g. a DoG detector) sized its
411
+ own tile differently — `relate.py` detects a chunk mismatch per pair and
412
+ rechunks the finer side to match before relating, so this is no longer
413
+ something you need to plan around. (`run_multi.py` *also* auto-pins
414
+ every config under `"auto"` to one shared, smallest-computed tile
415
+ up front — mainly useful to keep segmentation itself GPU-efficient
416
+ across configs, not required for the relate step to work.)
415
417
 
416
418
  See [Relating labels across segmentations](label_relations.md) for what
417
419
  `label_relations()` returns and how to save it yourself — the cluster
@@ -8,6 +8,8 @@ sys.path.insert(
8
8
  0, str(Path(__file__).resolve().parents[1] / "workflow" / "scripts")
9
9
  )
10
10
 
11
+ import numpy as np # noqa: E402
12
+ import openpyxl # noqa: E402
11
13
  import pytest # noqa: E402
12
14
  import yaml # noqa: E402
13
15
 
@@ -186,6 +188,64 @@ def test_relate_script_has_the_real_bookkeeping():
186
188
  assert "openpyxl" in src
187
189
 
188
190
 
191
+ def test_relate_rechunks_mismatched_label_arrays(tmp_path):
192
+ """A chunk-layout mismatch must be rechunked away, not require a re-run.
193
+
194
+ Two configs are free to have segmented at different tile_shape (one
195
+ published before the other's config changed, or a cheaper method sized
196
+ its own tile differently) -- label_relations() itself refuses mismatched
197
+ chunks by design, but that only means the caller has to rechunk one side
198
+ first, not that the whole segmentation needs redoing.
199
+ """
200
+ import zarr
201
+
202
+ from relate import run_relations
203
+
204
+ image_store = str(tmp_path / "image.zarr")
205
+
206
+ # a: labels 1 and 2, split at x=5. b: a single label 10 covering all of
207
+ # a's label 1 and none of label 2 -- built with a *different* chunking.
208
+ a_data = np.zeros((1, 10), dtype=np.int32)
209
+ a_data[0, :5] = 1
210
+ a_data[0, 5:] = 2
211
+ b_data = np.zeros((1, 10), dtype=np.int32)
212
+ b_data[0, :5] = 10
213
+
214
+ root = zarr.open_group(image_store, mode="w")
215
+ labels = root.require_group("labels")
216
+ a_grp = labels.require_group("nuclei_labels")
217
+ a_arr = a_grp.create_array(
218
+ name="0", shape=a_data.shape, chunks=(1, 2), dtype=np.int32
219
+ )
220
+ a_arr[:] = a_data
221
+ a_grp.attrs["sequential_labels"] = True
222
+ a_grp.attrs["n_objects"] = 2
223
+
224
+ b_grp = labels.require_group("cyto_labels")
225
+ b_arr = b_grp.create_array(
226
+ name="0", shape=b_data.shape, chunks=(1, 5), dtype=np.int32
227
+ )
228
+ b_arr[:] = b_data
229
+ b_grp.attrs["sequential_labels"] = True
230
+ b_grp.attrs["n_objects"] = 1
231
+
232
+ out_dir = tmp_path / "work"
233
+ out_dir.mkdir()
234
+ run_relations(
235
+ str(out_dir),
236
+ image_store,
237
+ [{"a": "nuclei_labels", "b": "cyto_labels", "output": "rel.xlsx"}],
238
+ )
239
+
240
+ wb = openpyxl.load_workbook(out_dir / "rel.xlsx")
241
+ rows = {
242
+ row[0]: (row[1], row[2], row[3])
243
+ for row in wb["nuclei_labels"].iter_rows(min_row=2, values_only=True)
244
+ }
245
+ assert rows[1] == (10, 5, 1.0) # label 1 fully inside b's label 10
246
+ assert rows[2] == (None, 0, 0) # label 2 touches nothing in b
247
+
248
+
189
249
  def test_mixed_nuclei_channel_auto_passes_validation():
190
250
  """A channel-count mismatch under `tile_shape: "auto"` is no longer
191
251
 
@@ -16,9 +16,15 @@ from __future__ import annotations
16
16
 
17
17
  import argparse
18
18
  import json
19
+ import math
19
20
  from pathlib import Path
20
21
 
21
22
 
23
+ def _num_chunks(arr: "da.Array") -> int: # noqa: F821 - dask imported lazily by callers
24
+ """Total chunk count of a dask array, for picking the coarser side to rechunk to."""
25
+ return math.prod(len(c) for c in arr.chunks)
26
+
27
+
22
28
  def _label_ids(image_store: str, name: str) -> list[int]:
23
29
  """Ids present in a label image, without scanning the volume.
24
30
 
@@ -72,6 +78,35 @@ def run_relations(
72
78
  print(f"[relate] relating {a_name} -> {b_name} …", flush=True)
73
79
  a = da.from_zarr(image_store, component=f"labels/{a_name}/0")
74
80
  b = da.from_zarr(image_store, component=f"labels/{b_name}/0")
81
+
82
+ # label_relations() requires matching chunk layouts (it walks both
83
+ # arrays block-by-block at the same index) but two configs are free
84
+ # to have segmented at different tile_shape -- e.g. one already
85
+ # published before the other's config changed, or a cheaper method
86
+ # naturally sized its tile differently. Same shape, different
87
+ # chunking is a normal dask op (extra I/O reading across misaligned
88
+ # source chunks, not a correctness issue), so rechunk the finer side
89
+ # to the coarser one here rather than require identical tile_shape
90
+ # across every config up front.
91
+ if a.chunks != b.chunks:
92
+ a_n, b_n = _num_chunks(a), _num_chunks(b)
93
+ if a_n <= b_n:
94
+ print(
95
+ f"[relate] {a_name} chunks {a.chunks} != {b_name} "
96
+ f"chunks {b.chunks}; rechunking {b_name} to match "
97
+ f"{a_name} (fewer chunks)",
98
+ flush=True,
99
+ )
100
+ b = b.rechunk(a.chunks)
101
+ else:
102
+ print(
103
+ f"[relate] {a_name} chunks {a.chunks} != {b_name} "
104
+ f"chunks {b.chunks}; rechunking {a_name} to match "
105
+ f"{b_name} (fewer chunks)",
106
+ flush=True,
107
+ )
108
+ a = a.rechunk(b.chunks)
109
+
75
110
  table = label_relations(a, b)
76
111
 
77
112
  # label_relations() only returns a-objects that touch a b-object.
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