patchworks 0.11.18__tar.gz → 0.11.20__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 (77) hide show
  1. {patchworks-0.11.18 → patchworks-0.11.20}/PKG-INFO +3 -1
  2. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/snakemake.md +43 -0
  3. {patchworks-0.11.18 → patchworks-0.11.20}/pyproject.toml +4 -0
  4. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_relations.py +9 -3
  5. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/plugins/napari.py +3 -1
  6. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/README.md +9 -0
  7. patchworks-0.11.20/workflow/config/config_cyto.yaml +33 -0
  8. patchworks-0.11.20/workflow/config/config_nuclei.yaml +33 -0
  9. patchworks-0.11.20/workflow/config/multi.yaml +21 -0
  10. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/pixi.toml +14 -3
  11. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/merge.py +3 -1
  12. patchworks-0.11.20/workflow/scripts/run_multi.py +160 -0
  13. {patchworks-0.11.18 → patchworks-0.11.20}/.github/workflows/docs.yml +0 -0
  14. {patchworks-0.11.18 → patchworks-0.11.20}/.github/workflows/lint.yml +0 -0
  15. {patchworks-0.11.18 → patchworks-0.11.20}/.github/workflows/release.yml +0 -0
  16. {patchworks-0.11.18 → patchworks-0.11.20}/.gitignore +0 -0
  17. {patchworks-0.11.18 → patchworks-0.11.20}/.markdownlint-cli2.yaml +0 -0
  18. {patchworks-0.11.18 → patchworks-0.11.20}/LICENSE +0 -0
  19. {patchworks-0.11.18 → patchworks-0.11.20}/README.md +0 -0
  20. {patchworks-0.11.18 → patchworks-0.11.20}/cliff.toml +0 -0
  21. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/chunks.md +0 -0
  22. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/cluster.md +0 -0
  23. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/io.md +0 -0
  24. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/merge_tile_labels.md +0 -0
  25. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/plugins/cellpose.md +0 -0
  26. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/plugins/napari.md +0 -0
  27. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/plugins/ome_zarr.md +0 -0
  28. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/relabel.md +0 -0
  29. {patchworks-0.11.18 → patchworks-0.11.20}/docs/api/tile_process.md +0 -0
  30. {patchworks-0.11.18 → patchworks-0.11.20}/docs/assets/logo.png +0 -0
  31. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/cellpose_2d.md +0 -0
  32. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/cellpose_2d.py +0 -0
  33. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/cellpose_3d.md +0 -0
  34. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/cellpose_3d.py +0 -0
  35. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/custom.md +0 -0
  36. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/custom_method.py +0 -0
  37. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/standalone_merge.md +0 -0
  38. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/stardist.md +0 -0
  39. {patchworks-0.11.18 → patchworks-0.11.20}/docs/examples/stardist_2d.py +0 -0
  40. {patchworks-0.11.18 → patchworks-0.11.20}/docs/getting_started.md +0 -0
  41. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/gpu_distributed.md +0 -0
  42. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/merging.md +0 -0
  43. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/ome_zarr_napari.md +0 -0
  44. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/performance.md +0 -0
  45. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/pitfalls.md +0 -0
  46. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/skip_empty.md +0 -0
  47. {patchworks-0.11.18 → patchworks-0.11.20}/docs/guide/tiling.md +0 -0
  48. {patchworks-0.11.18 → patchworks-0.11.20}/docs/index.md +0 -0
  49. {patchworks-0.11.18 → patchworks-0.11.20}/mkdocs.yml +0 -0
  50. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/__init__.py +0 -0
  51. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_chunks.py +0 -0
  52. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_cluster.py +0 -0
  53. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_core.py +0 -0
  54. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_distributed.py +0 -0
  55. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_io.py +0 -0
  56. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_merge.py +0 -0
  57. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/_relabel.py +0 -0
  58. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/plugins/__init__.py +0 -0
  59. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/plugins/cellpose.py +0 -0
  60. {patchworks-0.11.18 → patchworks-0.11.20}/src/patchworks/plugins/ome_zarr.py +0 -0
  61. {patchworks-0.11.18 → patchworks-0.11.20}/tests/test_core.py +0 -0
  62. {patchworks-0.11.18 → patchworks-0.11.20}/tests/test_distributed.py +0 -0
  63. {patchworks-0.11.18 → patchworks-0.11.20}/tests/test_napari.py +0 -0
  64. {patchworks-0.11.18 → patchworks-0.11.20}/tests/test_ome_zarr.py +0 -0
  65. {patchworks-0.11.18 → patchworks-0.11.20}/tests/test_relations.py +0 -0
  66. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/Snakefile +0 -0
  67. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/config/config.yaml +0 -0
  68. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/profile/slurm/config.yaml +0 -0
  69. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/rules/common.smk +0 -0
  70. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/rules/convert.smk +0 -0
  71. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/rules/merge.smk +0 -0
  72. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/rules/segment.smk +0 -0
  73. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/_pw.py +0 -0
  74. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/convert.py +0 -0
  75. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/fetch_model.py +0 -0
  76. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/prepare_tiles.py +0 -0
  77. {patchworks-0.11.18 → patchworks-0.11.20}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 0.11.18
3
+ Version: 0.11.20
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
@@ -34,6 +34,7 @@ Requires-Dist: glasbey; extra == 'all'
34
34
  Requires-Dist: imaris-ims-file-reader; extra == 'all'
35
35
  Requires-Dist: ipykernel<7; extra == 'all'
36
36
  Requires-Dist: lxml-html-clean; extra == 'all'
37
+ Requires-Dist: napari-dask-ndmeasure>=0.1.0; extra == 'all'
37
38
  Requires-Dist: napari[all]>=0.7.1; extra == 'all'
38
39
  Requires-Dist: numpy<2.5; extra == 'all'
39
40
  Requires-Dist: nvidia-ml-py; extra == 'all'
@@ -74,6 +75,7 @@ Provides-Extra: napari
74
75
  Requires-Dist: glasbey; extra == 'napari'
75
76
  Requires-Dist: ipykernel<7; extra == 'napari'
76
77
  Requires-Dist: lxml-html-clean; extra == 'napari'
78
+ Requires-Dist: napari-dask-ndmeasure>=0.1.0; extra == 'napari'
77
79
  Requires-Dist: napari[all]>=0.7.1; extra == 'napari'
78
80
  Requires-Dist: numpy<2.5; extra == 'napari'
79
81
  Provides-Extra: workflow
@@ -236,6 +236,12 @@ snakemake --workflow-profile profile/slurm --configfile config/config_nuclei.yam
236
236
  snakemake --workflow-profile profile/slurm --configfile config/config_cyto.yaml
237
237
  ```
238
238
 
239
+ !!! tip "One command for several segmentations + relations"
240
+ `config/multi.yaml` lists any number of segmentation configs plus which
241
+ pairs to relate afterward; `pixi run multi` (or `multi-slurm`) runs them
242
+ in order and writes a CSV per pair — see *One command: multiple
243
+ segmentations + relations* below for the config format.
244
+
239
245
  Both land side by side in the same store:
240
246
 
241
247
  ```text
@@ -281,6 +287,43 @@ with open("nuclei_to_cell.csv", "w", newline="") as f:
281
287
  w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
282
288
  ```
283
289
 
290
+ ### One command: multiple segmentations + relations
291
+
292
+ `scripts/run_multi.py` (wired up as `pixi run multi`) sequences the above
293
+ manually: run every segmentation config listed, then compute + save every
294
+ configured relation — one command instead of juggling several `snakemake`
295
+ calls and a separate Python step.
296
+
297
+ ```yaml
298
+ # config/multi.yaml
299
+ segmentations:
300
+ - config/config_nuclei.yaml
301
+ - config/config_cyto.yaml
302
+
303
+ relations:
304
+ - a: nuclei_labels
305
+ b: cyto_labels
306
+ output: nuclei_to_cyto.csv # written into work_dir
307
+ ```
308
+
309
+ ```bash
310
+ pixi run multi-dry # dry-run every segmentation config (skips relations)
311
+ pixi run multi # run locally
312
+ pixi run multi-slurm # submit every segmentation to SLURM
313
+ ```
314
+
315
+ Every listed segmentation config must share the same `work_dir` (so
316
+ `label_relations` has one `image.zarr` to read both label groups from) — the
317
+ script checks this and errors out otherwise. `relations` is optional; omit it
318
+ to just chain segmentations without a relation step.
319
+
320
+ Both lists are ordinary lists, so 3+ segmentations work the same way — add
321
+ more entries to `segmentations`, then list whichever pairs to relate. There's
322
+ no automatic "chain": list every pair explicitly, e.g. for nuclei + cyto +
323
+ membrane you'd add `nuclei_labels -> cyto_labels`, `nuclei_labels ->
324
+ membrane_labels`, and `cyto_labels -> membrane_labels` as three separate
325
+ entries under `relations`.
326
+
284
327
  ## Measurements (fast, whole-volume regionprops)
285
328
 
286
329
  `skimage.measure.regionprops` needs the full labelled + intensity array in
@@ -67,12 +67,16 @@ imaris = ["imaris-ims-file-reader"]
67
67
  # - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
68
68
  # into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
69
69
  # - glasbey: distinct high-contrast label LUTs for view_in_napari.
70
+ # - napari-dask-ndmeasure: out-of-core regionprops-style measurements
71
+ # (area/centroid/intensity stats) for huge Labels layers, straight off
72
+ # their backing dask/zarr arrays — the "Measure" dock widget.
70
73
  napari = [
71
74
  "napari[all]>=0.7.1",
72
75
  "numpy<2.5",
73
76
  "ipykernel<7",
74
77
  "lxml-html-clean",
75
78
  "glasbey",
79
+ "napari-dask-ndmeasure>=0.1.0",
76
80
  ]
77
81
  # workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
78
82
  workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm"]
@@ -14,7 +14,9 @@ import numpy as np
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
- def _as_dask(source: Union["da.Array", str, Path], component: str) -> "da.Array":
17
+ def _as_dask(
18
+ source: Union["da.Array", str, Path], component: str
19
+ ) -> "da.Array":
18
20
  if isinstance(source, (str, Path)):
19
21
  return da.from_zarr(str(source), component=component)
20
22
  return source
@@ -96,7 +98,9 @@ def label_relations(
96
98
 
97
99
  def _one(flat_idx: int) -> np.ndarray:
98
100
  idx = np.unravel_index(flat_idx, n_blocks)
99
- return _chunk_pairs(np.asarray(a.blocks[idx]), np.asarray(b.blocks[idx]))
101
+ return _chunk_pairs(
102
+ np.asarray(a.blocks[idx]), np.asarray(b.blocks[idx])
103
+ )
100
104
 
101
105
  with ThreadPoolExecutor(max_workers=nw) as ex:
102
106
  parts = list(ex.map(_one, range(total)))
@@ -130,7 +134,9 @@ def label_relations(
130
134
  best[a_id] = (b_id, count)
131
135
 
132
136
  logger.info(
133
- "label_relations: %d a-labels matched across %d chunks", len(best), total
137
+ "label_relations: %d a-labels matched across %d chunks",
138
+ len(best),
139
+ total,
134
140
  )
135
141
  return {
136
142
  a_id: {
@@ -299,7 +299,9 @@ def view_in_napari(
299
299
  img = _resolve_image(image, channel)
300
300
  img_ndim = img[0].ndim if isinstance(img, list) else img.ndim
301
301
  img_scale, img_units = (
302
- _pyramid_calibration(image, img_ndim) if _is_zarr(image) else (None, None)
302
+ _pyramid_calibration(image, img_ndim)
303
+ if _is_zarr(image)
304
+ else (None, None)
303
305
  )
304
306
  viewer = napari.Viewer()
305
307
  viewer.add_image(
@@ -86,6 +86,15 @@ the one converted `image.zarr` and land side by side in
86
86
  for the two-config recipe and `patchworks.label_relations()` for mapping one
87
87
  segmentation onto the other (e.g. nucleus → containing cell).
88
88
 
89
+ One command instead of juggling several manual runs: list your configs (and
90
+ which label pairs to relate) in `config/multi.yaml`, then:
91
+
92
+ ```bash
93
+ pixi run multi-dry # dry-run every segmentation config
94
+ pixi run multi # run locally
95
+ pixi run multi-slurm # submit every segmentation to SLURM
96
+ ```
97
+
89
98
  ## Output
90
99
 
91
100
  `<work_dir>/image.zarr` — the image plus `labels/<label_name>/` (multi-scale,
@@ -0,0 +1,33 @@
1
+ # Example segmentation config: cytoplasm/membrane channel.
2
+ # Paired with config_nuclei.yaml via config/multi.yaml — see
3
+ # docs/guide/snakemake.md "Running two segmentations". Same work_dir as
4
+ # config_nuclei.yaml, and the same tile_shape so patchworks.label_relations()
5
+ # can compare the two label arrays chunk-for-chunk.
6
+
7
+ input: "/path/to/scan.ims"
8
+ work_dir: "/path/to/results"
9
+
10
+ reuse_pyramid: false
11
+ convert_chunks: null
12
+ shard: false
13
+
14
+ channel: 0 # cytoplasm/membrane channel
15
+ level: 0
16
+ tile_shape: [16, 1024, 1024] # keep identical to config_nuclei.yaml
17
+ gpu_memory_gb: null
18
+ overlap: 30
19
+ skip_empty: true
20
+ empty_threshold: null
21
+
22
+ method: "cellpose"
23
+ label_name: "cyto_labels"
24
+ cellpose:
25
+ model: "cyto3"
26
+ diameter: 30
27
+ do_3D: true
28
+ gpu: true
29
+
30
+ pyramid_levels: 5
31
+ pyramid_downscale: 2
32
+ sequential_labels: true
33
+ merge_workers: null
@@ -0,0 +1,33 @@
1
+ # Example segmentation config: nuclei channel.
2
+ # Paired with config_cyto.yaml via config/multi.yaml — see
3
+ # docs/guide/snakemake.md "Running two segmentations". Both configs share
4
+ # work_dir (and thus image.zarr) with config_cyto.yaml, but keep tile_shape
5
+ # identical across the two so patchworks.label_relations() can compare them.
6
+
7
+ input: "/path/to/scan.ims"
8
+ work_dir: "/path/to/results"
9
+
10
+ reuse_pyramid: false
11
+ convert_chunks: null
12
+ shard: false
13
+
14
+ channel: 1 # nuclear stain channel
15
+ level: 0
16
+ tile_shape: [16, 1024, 1024] # keep identical to config_cyto.yaml
17
+ gpu_memory_gb: null
18
+ overlap: 30
19
+ skip_empty: true
20
+ empty_threshold: null
21
+
22
+ method: "cellpose"
23
+ label_name: "nuclei_labels"
24
+ cellpose:
25
+ model: "nuclei"
26
+ diameter: 15
27
+ do_3D: true
28
+ gpu: true
29
+
30
+ pyramid_levels: 5
31
+ pyramid_downscale: 2
32
+ sequential_labels: true
33
+ merge_workers: null
@@ -0,0 +1,21 @@
1
+ # Drives `pixi run multi` / `scripts/run_multi.py`: runs several segmentation
2
+ # configs (in order) against the same work_dir, then relates their labels by
3
+ # voxel overlap (patchworks.label_relations) and writes a CSV per pair.
4
+ #
5
+ # All listed segmentation configs must share the same work_dir/image.zarr and
6
+ # the same tile_shape/level, so the resulting label arrays are directly
7
+ # comparable — see docs/guide/snakemake.md "Running two segmentations".
8
+ #
9
+ # Both lists are plain lists: 3+ segmentations work the same way — add more
10
+ # entries to `segmentations`, then list whichever pairs you want related in
11
+ # `relations` (there's no automatic "chain"; list every pair explicitly, e.g.
12
+ # nuclei->cyto, nuclei->membrane, cyto->membrane).
13
+
14
+ segmentations:
15
+ - config/config_nuclei.yaml
16
+ - config/config_cyto.yaml
17
+
18
+ relations:
19
+ - a: nuclei_labels
20
+ b: cyto_labels
21
+ output: nuclei_to_cyto.csv
@@ -1,8 +1,11 @@
1
1
  # pixi environment for the patchworks Snakemake workflow.
2
2
  # pixi install
3
- # pixi run dry # dry-run (plan only)
4
- # pixi run go # run locally
5
- # pixi run slurm # submit to SLURM (edit profile/slurm/config.yaml first)
3
+ # pixi run dry # dry-run (plan only)
4
+ # pixi run go # run locally
5
+ # pixi run slurm # submit to SLURM (edit profile/slurm/config.yaml first)
6
+ # pixi run multi # run several segmentations + relate their labels (local)
7
+ # pixi run multi-slurm # same, on SLURM
8
+ # pixi run multi-dry # dry-run every segmentation in config/multi.yaml
6
9
  #
7
10
  # On a cluster, put this project on a shared filesystem the compute nodes can
8
11
  # read: the SLURM jobs re-launch snakemake from this env's interpreter.
@@ -38,3 +41,11 @@ cellpose4 = { features = ["cp4"], solve-group = "cp4" }
38
41
  dry = "snakemake -s Snakefile --configfile config/config.yaml --rerun-triggers mtime -n -p"
39
42
  go = "snakemake -s Snakefile --configfile config/config.yaml --rerun-triggers mtime --cores 8"
40
43
  slurm = "snakemake --workflow-profile profile/slurm --configfile config/config.yaml"
44
+
45
+ # Run every segmentation config listed in config/multi.yaml (in order), then
46
+ # relate their labels by voxel overlap (patchworks.label_relations) and write
47
+ # a CSV per configured pair — see config/multi.yaml and
48
+ # docs/guide/snakemake.md "Running two segmentations".
49
+ multi-dry = "python scripts/run_multi.py --config config/multi.yaml -n"
50
+ multi = "python scripts/run_multi.py --config config/multi.yaml"
51
+ multi-slurm = "python scripts/run_multi.py --config config/multi.yaml --profile profile/slurm"
@@ -25,7 +25,9 @@ merged_store = str(Path(work_dir) / label_name / "_merged.zarr")
25
25
  # whatever cpus_per_task the "merge" rule was actually allocated in the SLURM
26
26
  # profile. Read the real allocation (SLURM_CPUS_PER_TASK) so the job uses all
27
27
  # the cores it's paying for; merge_workers: in config.yaml can still override.
28
- default_workers = int(os.environ.get("SLURM_CPUS_PER_TASK", os.cpu_count() or 4))
28
+ default_workers = int(
29
+ os.environ.get("SLURM_CPUS_PER_TASK", os.cpu_count() or 4)
30
+ )
29
31
  merged = merge_tile_labels(
30
32
  stage_path(work_dir, label_name),
31
33
  write_to=merged_store,
@@ -0,0 +1,160 @@
1
+ """Run several segmentation configs, then relate their labels by overlap.
2
+
3
+ Usage:
4
+ python scripts/run_multi.py --config config/multi.yaml
5
+ python scripts/run_multi.py --config config/multi.yaml --profile profile/slurm
6
+ python scripts/run_multi.py --config config/multi.yaml -n # dry-run only
7
+
8
+ See config/multi.yaml and docs/guide/snakemake.md "Running two segmentations"
9
+ for the config format. Each listed segmentation config is run as an ordinary
10
+ `snakemake --configfile ...` invocation (this script is a thin sequencer, not
11
+ a Snakemake rule — the segmentations already namespace their own paths under
12
+ work_dir/<label_name>/, so running them one after another here is exactly
13
+ equivalent to running each snakemake command by hand). Once all segmentations
14
+ finish, each configured relation pair is computed via
15
+ patchworks.label_relations and written as a CSV in work_dir.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import csv
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ import yaml
27
+
28
+
29
+ def _load_yaml(path: Path) -> dict:
30
+ return yaml.safe_load(path.read_text())
31
+
32
+
33
+ def _run_snakemake(
34
+ configfile: Path,
35
+ *,
36
+ workflow_dir: Path,
37
+ profile: str | None,
38
+ cores: int,
39
+ dry_run: bool,
40
+ ) -> None:
41
+ cmd = [
42
+ "snakemake",
43
+ "-s",
44
+ str(workflow_dir / "Snakefile"),
45
+ "--configfile",
46
+ str(configfile),
47
+ ]
48
+ if profile:
49
+ cmd += ["--workflow-profile", profile]
50
+ else:
51
+ cmd += ["--cores", str(cores), "--rerun-triggers", "mtime"]
52
+ if dry_run:
53
+ cmd += ["-n", "-p"]
54
+ print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
55
+ subprocess.run(cmd, check=True, cwd=workflow_dir)
56
+
57
+
58
+ def _resolve(workflow_dir: Path, path_str: str) -> Path:
59
+ path = Path(path_str)
60
+ return path if path.is_absolute() else workflow_dir / path
61
+
62
+
63
+ def main() -> None:
64
+ parser = argparse.ArgumentParser(
65
+ description=__doc__,
66
+ formatter_class=argparse.RawDescriptionHelpFormatter,
67
+ )
68
+ parser.add_argument(
69
+ "--config", required=True, help="multi-segmentation config YAML"
70
+ )
71
+ parser.add_argument(
72
+ "--profile",
73
+ default=None,
74
+ help="Snakemake --workflow-profile (e.g. profile/slurm); omit to run locally",
75
+ )
76
+ parser.add_argument(
77
+ "--cores",
78
+ type=int,
79
+ default=8,
80
+ help="local run: --cores (ignored with --profile)",
81
+ )
82
+ parser.add_argument(
83
+ "-n",
84
+ "--dry-run",
85
+ action="store_true",
86
+ help="pass -n -p to every Snakemake run; skips relations",
87
+ )
88
+ args = parser.parse_args()
89
+
90
+ workflow_dir = Path(__file__).resolve().parent.parent
91
+ multi_cfg_path = _resolve(workflow_dir, args.config)
92
+ multi_cfg = _load_yaml(multi_cfg_path)
93
+
94
+ seg_config_paths = [
95
+ _resolve(workflow_dir, c) for c in multi_cfg["segmentations"]
96
+ ]
97
+ for cfg_path in seg_config_paths:
98
+ _run_snakemake(
99
+ cfg_path,
100
+ workflow_dir=workflow_dir,
101
+ profile=args.profile,
102
+ cores=args.cores,
103
+ dry_run=args.dry_run,
104
+ )
105
+
106
+ relations = multi_cfg.get("relations", [])
107
+ if args.dry_run or not relations:
108
+ return
109
+
110
+ seg_cfgs = [_load_yaml(p) for p in seg_config_paths]
111
+ work_dirs = {cfg["work_dir"] for cfg in seg_cfgs}
112
+ if len(work_dirs) != 1:
113
+ print(
114
+ f"[run_multi] ERROR: segmentation configs use different work_dir "
115
+ f"({sorted(work_dirs)}); label_relations needs one shared "
116
+ "image.zarr to compare against.",
117
+ file=sys.stderr,
118
+ )
119
+ sys.exit(1)
120
+ work_dir = work_dirs.pop()
121
+ image_store = f"{work_dir}/image.zarr"
122
+
123
+ import dask.array as da
124
+
125
+ from patchworks import label_relations
126
+
127
+ for rel in relations:
128
+ a_name, b_name = rel["a"], rel["b"]
129
+ out_path = Path(work_dir) / rel.get(
130
+ "output", f"{a_name}_to_{b_name}.csv"
131
+ )
132
+ print(f"[run_multi] relating {a_name} -> {b_name} …", flush=True)
133
+ a = da.from_zarr(image_store, component=f"labels/{a_name}/0")
134
+ b = da.from_zarr(image_store, component=f"labels/{b_name}/0")
135
+ table = label_relations(a, b)
136
+
137
+ with open(out_path, "w", newline="") as f:
138
+ writer = csv.writer(f)
139
+ writer.writerow(
140
+ [
141
+ f"{a_name}_id",
142
+ f"{b_name}_id",
143
+ "overlap_voxels",
144
+ "overlap_fraction",
145
+ ]
146
+ )
147
+ for a_id, m in table.items():
148
+ writer.writerow(
149
+ [
150
+ a_id,
151
+ m["match"],
152
+ m["overlap_voxels"],
153
+ m["overlap_fraction"],
154
+ ]
155
+ )
156
+ print(f"[run_multi] wrote {out_path} ({len(table)} rows)", flush=True)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes