patchworks 2.6.5__tar.gz → 2.6.7__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.7}/PKG-INFO +1 -1
  2. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/label_relations.md +15 -1
  3. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/snakemake.md +21 -12
  4. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_run_multi.py +73 -0
  5. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/relate.py +53 -0
  6. {patchworks-2.6.5 → patchworks-2.6.7}/.github/workflows/docs.yml +0 -0
  7. {patchworks-2.6.5 → patchworks-2.6.7}/.github/workflows/lint.yml +0 -0
  8. {patchworks-2.6.5 → patchworks-2.6.7}/.github/workflows/release.yml +0 -0
  9. {patchworks-2.6.5 → patchworks-2.6.7}/.gitignore +0 -0
  10. {patchworks-2.6.5 → patchworks-2.6.7}/.markdownlint-cli2.yaml +0 -0
  11. {patchworks-2.6.5 → patchworks-2.6.7}/LICENSE +0 -0
  12. {patchworks-2.6.5 → patchworks-2.6.7}/README.md +0 -0
  13. {patchworks-2.6.5 → patchworks-2.6.7}/cliff.toml +0 -0
  14. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/chunks.md +0 -0
  15. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/cluster.md +0 -0
  16. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/io.md +0 -0
  17. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/merge_tile_labels.md +0 -0
  18. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/plugins/cellpose.md +0 -0
  19. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/plugins/dog.md +0 -0
  20. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/plugins/napari.md +0 -0
  21. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/plugins/ome_zarr.md +0 -0
  22. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/postprocess.md +0 -0
  23. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/relabel.md +0 -0
  24. {patchworks-2.6.5 → patchworks-2.6.7}/docs/api/tile_process.md +0 -0
  25. {patchworks-2.6.5 → patchworks-2.6.7}/docs/assets/logo.png +0 -0
  26. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/cellpose_2d.md +0 -0
  27. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/cellpose_2d.py +0 -0
  28. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/cellpose_3d.md +0 -0
  29. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/cellpose_3d.py +0 -0
  30. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/custom.md +0 -0
  31. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/custom_method.py +0 -0
  32. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/dog.md +0 -0
  33. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/dog.py +0 -0
  34. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/standalone_merge.md +0 -0
  35. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/stardist.md +0 -0
  36. {patchworks-2.6.5 → patchworks-2.6.7}/docs/examples/stardist_2d.py +0 -0
  37. {patchworks-2.6.5 → patchworks-2.6.7}/docs/getting_started.md +0 -0
  38. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/custom_segmentation.md +0 -0
  39. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/gpu_distributed.md +0 -0
  40. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/measurements.md +0 -0
  41. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/merging.md +0 -0
  42. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/ome_zarr_napari.md +0 -0
  43. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/performance.md +0 -0
  44. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/pitfalls.md +0 -0
  45. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/skip_empty.md +0 -0
  46. {patchworks-2.6.5 → patchworks-2.6.7}/docs/guide/tiling.md +0 -0
  47. {patchworks-2.6.5 → patchworks-2.6.7}/docs/index.md +0 -0
  48. {patchworks-2.6.5 → patchworks-2.6.7}/mkdocs.yml +0 -0
  49. {patchworks-2.6.5 → patchworks-2.6.7}/pyproject.toml +0 -0
  50. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/__init__.py +0 -0
  51. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_chunks.py +0 -0
  52. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_cluster.py +0 -0
  53. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_core.py +0 -0
  54. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_distributed.py +0 -0
  55. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_gpu.py +0 -0
  56. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_io.py +0 -0
  57. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_merge.py +0 -0
  58. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_notify.py +0 -0
  59. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_occupancy.py +0 -0
  60. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_postprocess.py +0 -0
  61. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_progress.py +0 -0
  62. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_relabel.py +0 -0
  63. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/_relations.py +0 -0
  64. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/plugins/__init__.py +0 -0
  65. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/plugins/cellpose.py +0 -0
  66. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/plugins/dog.py +0 -0
  67. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/plugins/napari.py +0 -0
  68. {patchworks-2.6.5 → patchworks-2.6.7}/src/patchworks/plugins/ome_zarr.py +0 -0
  69. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_allocation.py +0 -0
  70. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_core.py +0 -0
  71. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_distributed.py +0 -0
  72. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_dog.py +0 -0
  73. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_gpu.py +0 -0
  74. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_napari.py +0 -0
  75. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_notify.py +0 -0
  76. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_occupancy.py +0 -0
  77. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_ome_zarr.py +0 -0
  78. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_postprocess.py +0 -0
  79. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_progress.py +0 -0
  80. {patchworks-2.6.5 → patchworks-2.6.7}/tests/test_relations.py +0 -0
  81. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/README.md +0 -0
  82. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/Snakefile +0 -0
  83. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/common.yaml +0 -0
  84. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/config.yaml +0 -0
  85. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/config_cilia.yaml +0 -0
  86. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/config_cyto.yaml +0 -0
  87. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/config_nuclei.yaml +0 -0
  88. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/config/multi.yaml +0 -0
  89. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/pixi.toml +0 -0
  90. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/profile/slurm/config.yaml +0 -0
  91. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/rules/common.smk +0 -0
  92. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/rules/convert.smk +0 -0
  93. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/rules/merge.smk +0 -0
  94. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/rules/segment.smk +0 -0
  95. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/_pw.py +0 -0
  96. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/build_occupancy.py +0 -0
  97. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/convert.py +0 -0
  98. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/fetch_model.py +0 -0
  99. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/merge.py +0 -0
  100. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/prepare_tiles.py +0 -0
  101. {patchworks-2.6.5 → patchworks-2.6.7}/workflow/scripts/run_multi.py +0 -0
  102. {patchworks-2.6.5 → patchworks-2.6.7}/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.7
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,29 @@ 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.)
417
+
418
+ !!! tip "The relate step's log"
419
+ Unlike `prepare`/`segment`/`merge`, the relate step isn't a Snakemake
420
+ rule, so it doesn't get a `log:` directive for free. It writes its own
421
+ log to `<work_dir>/logs/relate.log` (override with `relate.py --log`),
422
+ the same tee-to-file-and-stdout behaviour as the other steps — check
423
+ there instead of scrolling back through `srun`'s live output.
415
424
 
416
425
  See [Relating labels across segmentations](label_relations.md) for what
417
426
  `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,77 @@ def test_relate_script_has_the_real_bookkeeping():
186
188
  assert "openpyxl" in src
187
189
 
188
190
 
191
+ def test_relate_writes_its_own_log():
192
+ """relate.py runs via srun, not a Snakemake rule -- nothing else wires up
193
+
194
+ its logging (see prepare/segment/merge's `log:` directives), so main()
195
+ has to call start_log() itself or its output only ever streams to
196
+ whatever invoked srun and is gone once that terminal scrolls past it.
197
+ """
198
+ src = (_workflow_dir() / "scripts" / "relate.py").read_text()
199
+ assert "from _pw import start_log" in src
200
+ assert "start_log(" in src
201
+ assert '"logs" / "relate.log"' in src
202
+
203
+
204
+ def test_relate_rechunks_mismatched_label_arrays(tmp_path):
205
+ """A chunk-layout mismatch must be rechunked away, not require a re-run.
206
+
207
+ Two configs are free to have segmented at different tile_shape (one
208
+ published before the other's config changed, or a cheaper method sized
209
+ its own tile differently) -- label_relations() itself refuses mismatched
210
+ chunks by design, but that only means the caller has to rechunk one side
211
+ first, not that the whole segmentation needs redoing.
212
+ """
213
+ import zarr
214
+
215
+ from relate import run_relations
216
+
217
+ image_store = str(tmp_path / "image.zarr")
218
+
219
+ # a: labels 1 and 2, split at x=5. b: a single label 10 covering all of
220
+ # a's label 1 and none of label 2 -- built with a *different* chunking.
221
+ a_data = np.zeros((1, 10), dtype=np.int32)
222
+ a_data[0, :5] = 1
223
+ a_data[0, 5:] = 2
224
+ b_data = np.zeros((1, 10), dtype=np.int32)
225
+ b_data[0, :5] = 10
226
+
227
+ root = zarr.open_group(image_store, mode="w")
228
+ labels = root.require_group("labels")
229
+ a_grp = labels.require_group("nuclei_labels")
230
+ a_arr = a_grp.create_array(
231
+ name="0", shape=a_data.shape, chunks=(1, 2), dtype=np.int32
232
+ )
233
+ a_arr[:] = a_data
234
+ a_grp.attrs["sequential_labels"] = True
235
+ a_grp.attrs["n_objects"] = 2
236
+
237
+ b_grp = labels.require_group("cyto_labels")
238
+ b_arr = b_grp.create_array(
239
+ name="0", shape=b_data.shape, chunks=(1, 5), dtype=np.int32
240
+ )
241
+ b_arr[:] = b_data
242
+ b_grp.attrs["sequential_labels"] = True
243
+ b_grp.attrs["n_objects"] = 1
244
+
245
+ out_dir = tmp_path / "work"
246
+ out_dir.mkdir()
247
+ run_relations(
248
+ str(out_dir),
249
+ image_store,
250
+ [{"a": "nuclei_labels", "b": "cyto_labels", "output": "rel.xlsx"}],
251
+ )
252
+
253
+ wb = openpyxl.load_workbook(out_dir / "rel.xlsx")
254
+ rows = {
255
+ row[0]: (row[1], row[2], row[3])
256
+ for row in wb["nuclei_labels"].iter_rows(min_row=2, values_only=True)
257
+ }
258
+ assert rows[1] == (10, 5, 1.0) # label 1 fully inside b's label 10
259
+ assert rows[2] == (None, 0, 0) # label 2 touches nothing in b
260
+
261
+
189
262
  def test_mixed_nuclei_channel_auto_passes_validation():
190
263
  """A channel-count mismatch under `tile_shape: "auto"` is no longer
191
264
 
@@ -10,15 +10,27 @@ e.g. under srun):
10
10
  python scripts/relate.py --work-dir /path/to/work_dir \
11
11
  --image-store /path/to/work_dir/image.zarr \
12
12
  --relations '[{"a": "nuclei_labels", "b": "cyto_labels", "output": "nuclei_to_cyto.xlsx"}]'
13
+
14
+ Unlike prepare/segment/merge, this doesn't run as a Snakemake rule, so nothing
15
+ wires up its own logs/<rule>/*.log by default -- srun just streams output to
16
+ whatever invoked it. main() calls start_log() itself instead, writing to
17
+ <work_dir>/logs/relate.log (override with --log), the same tee-to-file-and-
18
+ stdout behaviour the Snakemake-driven scripts get via _pw.start_log.
13
19
  """
14
20
 
15
21
  from __future__ import annotations
16
22
 
17
23
  import argparse
18
24
  import json
25
+ import math
19
26
  from pathlib import Path
20
27
 
21
28
 
29
+ def _num_chunks(arr: "da.Array") -> int: # noqa: F821 - dask imported lazily by callers
30
+ """Total chunk count of a dask array, for picking the coarser side to rechunk to."""
31
+ return math.prod(len(c) for c in arr.chunks)
32
+
33
+
22
34
  def _label_ids(image_store: str, name: str) -> list[int]:
23
35
  """Ids present in a label image, without scanning the volume.
24
36
 
@@ -72,6 +84,35 @@ def run_relations(
72
84
  print(f"[relate] relating {a_name} -> {b_name} …", flush=True)
73
85
  a = da.from_zarr(image_store, component=f"labels/{a_name}/0")
74
86
  b = da.from_zarr(image_store, component=f"labels/{b_name}/0")
87
+
88
+ # label_relations() requires matching chunk layouts (it walks both
89
+ # arrays block-by-block at the same index) but two configs are free
90
+ # to have segmented at different tile_shape -- e.g. one already
91
+ # published before the other's config changed, or a cheaper method
92
+ # naturally sized its tile differently. Same shape, different
93
+ # chunking is a normal dask op (extra I/O reading across misaligned
94
+ # source chunks, not a correctness issue), so rechunk the finer side
95
+ # to the coarser one here rather than require identical tile_shape
96
+ # across every config up front.
97
+ if a.chunks != b.chunks:
98
+ a_n, b_n = _num_chunks(a), _num_chunks(b)
99
+ if a_n <= b_n:
100
+ print(
101
+ f"[relate] {a_name} chunks {a.chunks} != {b_name} "
102
+ f"chunks {b.chunks}; rechunking {b_name} to match "
103
+ f"{a_name} (fewer chunks)",
104
+ flush=True,
105
+ )
106
+ b = b.rechunk(a.chunks)
107
+ else:
108
+ print(
109
+ f"[relate] {a_name} chunks {a.chunks} != {b_name} "
110
+ f"chunks {b.chunks}; rechunking {a_name} to match "
111
+ f"{b_name} (fewer chunks)",
112
+ flush=True,
113
+ )
114
+ a = a.rechunk(b.chunks)
115
+
75
116
  table = label_relations(a, b)
76
117
 
77
118
  # label_relations() only returns a-objects that touch a b-object.
@@ -141,7 +182,19 @@ def main() -> None:
141
182
  "relations:"
142
183
  ),
143
184
  )
185
+ parser.add_argument(
186
+ "--log",
187
+ default=None,
188
+ help="log file path (default: <work-dir>/logs/relate.log)",
189
+ )
144
190
  args = parser.parse_args()
191
+
192
+ from _pw import start_log
193
+
194
+ log_path = args.log or str(Path(args.work_dir) / "logs" / "relate.log")
195
+ start_log(log_path)
196
+ print(f"[relate] logging to {log_path}", flush=True)
197
+
145
198
  run_relations(args.work_dir, args.image_store, json.loads(args.relations))
146
199
 
147
200
 
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