patchworks 2.2.0__tar.gz → 2.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.
- {patchworks-2.2.0 → patchworks-2.3.0}/PKG-INFO +1 -1
- {patchworks-2.2.0 → patchworks-2.3.0}/cliff.toml +2 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/snakemake.md +39 -10
- patchworks-2.3.0/tests/test_run_multi.py +123 -0
- patchworks-2.3.0/workflow/config/common.yaml +42 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/config/config_cilia.yaml +9 -27
- patchworks-2.3.0/workflow/config/config_cyto.yaml +22 -0
- patchworks-2.3.0/workflow/config/config_nuclei.yaml +22 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/config/multi.yaml +6 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/run_multi.py +53 -2
- patchworks-2.2.0/tests/test_run_multi.py +0 -38
- patchworks-2.2.0/workflow/config/config_cyto.yaml +0 -39
- patchworks-2.2.0/workflow/config/config_nuclei.yaml +0 -39
- {patchworks-2.2.0 → patchworks-2.3.0}/.github/workflows/docs.yml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/.github/workflows/lint.yml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/.github/workflows/release.yml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/.gitignore +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/LICENSE +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/README.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/chunks.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/cluster.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/io.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/postprocess.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/relabel.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/api/tile_process.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/assets/logo.png +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/custom.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/custom_method.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/dog.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/dog.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/stardist.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/getting_started.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/label_relations.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/measurements.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/merging.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/performance.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/guide/tiling.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/docs/index.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/mkdocs.yml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/pyproject.toml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/__init__.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_core.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_io.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_merge.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/_relations.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_allocation.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_core.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_distributed.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_dog.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_gpu.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_napari.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_occupancy.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_postprocess.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/tests/test_relations.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/README.md +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/Snakefile +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/config/config.yaml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/pixi.toml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/rules/common.smk +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/rules/convert.smk +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/rules/merge.smk +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/rules/segment.smk +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/convert.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/merge.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-2.2.0 → patchworks-2.3.0}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.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
|
|
@@ -31,6 +31,8 @@ commit_preprocessors = [
|
|
|
31
31
|
{ pattern = "(\\w+(?:\\([^)]*\\))?!?:\\s+)[^\\w\\s]+\\s*", replace = "$1" },
|
|
32
32
|
]
|
|
33
33
|
commit_parsers = [
|
|
34
|
+
# Merge commits are noise in a changelog — the merged commits are already listed.
|
|
35
|
+
{ message = "^Merge ", skip = true },
|
|
34
36
|
{ message = "^feat", group = "✨ Features" },
|
|
35
37
|
{ message = "^fix", group = "🐛 Bug Fixes" },
|
|
36
38
|
{ message = "^perf", group = "⚡ Performance" },
|
|
@@ -269,13 +269,26 @@ running the workflow **twice with two configs against the same `work_dir`**
|
|
|
269
269
|
never collides: each run gets its own private subdirectory, and both reuse
|
|
270
270
|
the *same* already-converted `image.zarr` (conversion never re-runs).
|
|
271
271
|
|
|
272
|
+
Most of what those configs contain is identical — the input, the `work_dir`,
|
|
273
|
+
the tiling, everything `convert` reads. Put it in **one** shared file and let
|
|
274
|
+
each config carry only what actually differs. Snakemake merges several
|
|
275
|
+
`--configfile` values in order, with the later one winning:
|
|
276
|
+
|
|
272
277
|
```yaml
|
|
273
|
-
# config/
|
|
278
|
+
# config/common.yaml — shared by every segmentation
|
|
274
279
|
input: "/data/scan.ims"
|
|
275
280
|
work_dir: "/scratch/results"
|
|
281
|
+
tile_shape: [16, 1024, 1024]
|
|
282
|
+
shard: false # true → far fewer files, same chunks
|
|
283
|
+
tiles_per_job: 4
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
# config/config_nuclei.yaml — only the differences
|
|
276
288
|
label_name: "nuclei_labels"
|
|
277
289
|
channel: 1 # nuclear stain channel
|
|
278
|
-
|
|
290
|
+
overlap: [4, 30, 30]
|
|
291
|
+
method: "cellpose"
|
|
279
292
|
cellpose:
|
|
280
293
|
model: "nuclei"
|
|
281
294
|
diameter: 15
|
|
@@ -283,12 +296,11 @@ cellpose:
|
|
|
283
296
|
```
|
|
284
297
|
|
|
285
298
|
```yaml
|
|
286
|
-
# config/config_cyto.yaml
|
|
287
|
-
input: "/data/scan.ims"
|
|
288
|
-
work_dir: "/scratch/results" # same work_dir — image.zarr is reused
|
|
299
|
+
# config/config_cyto.yaml — only the differences
|
|
289
300
|
label_name: "cyto_labels"
|
|
290
301
|
channel: 0 # cytoplasm/membrane channel
|
|
291
|
-
|
|
302
|
+
overlap: [4, 30, 30]
|
|
303
|
+
method: "cellpose"
|
|
292
304
|
cellpose:
|
|
293
305
|
model: "cyto3"
|
|
294
306
|
diameter: 30
|
|
@@ -300,12 +312,23 @@ they can run concurrently. Give each its own `--directory`, because
|
|
|
300
312
|
Snakemake's lock lives in the working directory, not in the config:
|
|
301
313
|
|
|
302
314
|
```bash
|
|
303
|
-
snakemake --workflow-profile profile/slurm --configfile config/config_nuclei.yaml
|
|
304
|
-
--directory /scratch/results/nuclei_labels/.snakemake
|
|
305
|
-
snakemake --workflow-profile profile/slurm --configfile config/config_cyto.yaml \
|
|
306
|
-
--directory /scratch/results/cyto_labels/.snakemake
|
|
315
|
+
snakemake --workflow-profile profile/slurm --configfile config/common.yaml config/config_nuclei.yaml --directory /scratch/results/nuclei_labels/.snakemake
|
|
307
316
|
```
|
|
308
317
|
|
|
318
|
+
```bash
|
|
319
|
+
snakemake --workflow-profile profile/slurm --configfile config/common.yaml config/config_cyto.yaml --directory /scratch/results/cyto_labels/.snakemake
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
!!! warning "Conversion settings belong in the shared file"
|
|
323
|
+
`convert` runs **once**, from the first config only. A `shard`, `input` or
|
|
324
|
+
`pyramid_levels` set on the second config is therefore never read, and
|
|
325
|
+
nothing logs that it was dropped. `run_multi` refuses to start when those
|
|
326
|
+
keys disagree across configs and tells you which one — but if you drive
|
|
327
|
+
the configs by hand, keep them in `common.yaml`.
|
|
328
|
+
|
|
329
|
+
Splitting the configs is optional: a self-contained config still works,
|
|
330
|
+
and `common:` can simply be left out of `multi.yaml`.
|
|
331
|
+
|
|
309
332
|
!!! tip "One command for several segmentations + relations"
|
|
310
333
|
`config/multi.yaml` lists any number of segmentation configs plus which
|
|
311
334
|
pairs to relate afterward; `pixi run multi` (or `multi-slurm`) converts
|
|
@@ -339,6 +362,8 @@ and saves every configured relation — one command instead of juggling several
|
|
|
339
362
|
|
|
340
363
|
```yaml
|
|
341
364
|
# config/multi.yaml
|
|
365
|
+
common: config/common.yaml # shared settings, merged under each config below
|
|
366
|
+
|
|
342
367
|
segmentations:
|
|
343
368
|
- config/config_nuclei.yaml
|
|
344
369
|
- config/config_cyto.yaml
|
|
@@ -349,6 +374,10 @@ relations:
|
|
|
349
374
|
output: nuclei_to_cyto.xlsx # written into work_dir
|
|
350
375
|
```
|
|
351
376
|
|
|
377
|
+
`common:` is optional — leave it out and each config must be self-contained,
|
|
378
|
+
as before. With it, changing the input path or turning on `shard` is a
|
|
379
|
+
one-line edit in one file instead of the same edit repeated per config.
|
|
380
|
+
|
|
352
381
|
```bash
|
|
353
382
|
pixi run multi-dry # dry-run every segmentation config (skips relations)
|
|
354
383
|
pixi run multi # run locally
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Tests for the multi-config driver's SLURM-facing behaviour."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.insert(
|
|
8
|
+
0, str(Path(__file__).resolve().parents[1] / "workflow" / "scripts")
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import pytest # noqa: E402
|
|
12
|
+
import yaml # noqa: E402
|
|
13
|
+
|
|
14
|
+
from run_multi import ( # noqa: E402
|
|
15
|
+
_CONVERT_KEYS,
|
|
16
|
+
_snakemake_cmd,
|
|
17
|
+
_validate_configs,
|
|
18
|
+
slurm_jobname_prefix,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# The SLURM executor's own rule (snakemake_executor_plugin_slurm): it raises a
|
|
22
|
+
# WorkflowError and aborts the whole run if the prefix does not match.
|
|
23
|
+
_EXECUTOR_RULE = re.compile(r"^[A-Za-z0-9_-]{1,50}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_jobname_prefix_satisfies_the_executor():
|
|
27
|
+
"""Whatever a label_name contains, the prefix must stay submittable.
|
|
28
|
+
|
|
29
|
+
The executor names jobs after a UUID and refuses a --job-name override, so
|
|
30
|
+
this prefix is the only thing that makes squeue readable -- and an invalid
|
|
31
|
+
one fails the run rather than degrading.
|
|
32
|
+
"""
|
|
33
|
+
for label in ("nuclei_labels", "cyto_labels", "convert", "a"):
|
|
34
|
+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix(label))
|
|
35
|
+
|
|
36
|
+
# Characters a label might plausibly pick up are sanitised, not passed on.
|
|
37
|
+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("cilia/v2 (test)"))
|
|
38
|
+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("run 1: nuclei"))
|
|
39
|
+
# And an over-long label is truncated to the executor's 50-char limit.
|
|
40
|
+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("x" * 200))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_jobname_prefix_keeps_the_label_readable():
|
|
44
|
+
"""The label must lead, since that is what a queue listing truncates to."""
|
|
45
|
+
assert slurm_jobname_prefix("nuclei_labels") == "pw-nuclei_labels"
|
|
46
|
+
assert slurm_jobname_prefix("convert") == "pw-convert"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_common_configfile_is_merged_under_the_per_config_one():
|
|
50
|
+
"""Snakemake merges --configfile values in order, later winning.
|
|
51
|
+
|
|
52
|
+
That ordering is the whole mechanism: shared settings come from common.yaml
|
|
53
|
+
and the per-config file overrides only what differs. Swap the two and every
|
|
54
|
+
config would silently get the shared defaults instead of its own channel.
|
|
55
|
+
"""
|
|
56
|
+
cmd = _snakemake_cmd(
|
|
57
|
+
Path("config/config_nuclei.yaml"),
|
|
58
|
+
workflow_dir=Path("workflow"),
|
|
59
|
+
profile=None,
|
|
60
|
+
cores=8,
|
|
61
|
+
dry_run=False,
|
|
62
|
+
common=Path("config/common.yaml"),
|
|
63
|
+
)
|
|
64
|
+
i = cmd.index("--configfile")
|
|
65
|
+
assert cmd[i + 1].endswith("common.yaml")
|
|
66
|
+
assert cmd[i + 2].endswith("config_nuclei.yaml")
|
|
67
|
+
|
|
68
|
+
# Without a common file the invocation is unchanged: one configfile, so
|
|
69
|
+
# a self-contained config keeps working exactly as before.
|
|
70
|
+
plain = _snakemake_cmd(
|
|
71
|
+
Path("config/config_nuclei.yaml"),
|
|
72
|
+
workflow_dir=Path("workflow"),
|
|
73
|
+
profile=None,
|
|
74
|
+
cores=8,
|
|
75
|
+
dry_run=False,
|
|
76
|
+
)
|
|
77
|
+
j = plain.index("--configfile")
|
|
78
|
+
assert plain[j + 1].endswith("config_nuclei.yaml")
|
|
79
|
+
assert not plain[j + 2].endswith(".yaml")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_convert_keys_must_agree_across_configs():
|
|
83
|
+
"""`convert` runs once from the first config, so a later one is ignored.
|
|
84
|
+
|
|
85
|
+
Setting shard on the second config and watching a million files appear
|
|
86
|
+
anyway is invisible without this check -- there is no log line saying the
|
|
87
|
+
value was dropped, because nothing ever read it.
|
|
88
|
+
"""
|
|
89
|
+
paths = [Path("a.yaml"), Path("b.yaml")]
|
|
90
|
+
base = {"work_dir": "/w", "tile_shape": [16, 512, 512], "level": 0}
|
|
91
|
+
good = [
|
|
92
|
+
{**base, "label_name": "a", "shard": True},
|
|
93
|
+
{**base, "label_name": "b", "shard": True},
|
|
94
|
+
]
|
|
95
|
+
assert _validate_configs(paths, good) == "/w"
|
|
96
|
+
|
|
97
|
+
bad = [
|
|
98
|
+
{**base, "label_name": "a", "shard": True},
|
|
99
|
+
{**base, "label_name": "b", "shard": False},
|
|
100
|
+
]
|
|
101
|
+
# It reports every problem and exits, rather than raising, so that a
|
|
102
|
+
# mistake costs one readable message instead of a traceback.
|
|
103
|
+
with pytest.raises(SystemExit):
|
|
104
|
+
_validate_configs(paths, bad)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_shipped_multi_configs_are_consistent():
|
|
108
|
+
"""The shipped example must satisfy its own validator.
|
|
109
|
+
|
|
110
|
+
It is the thing users copy, so a config set that run_multi would refuse to
|
|
111
|
+
start is worse than no example at all.
|
|
112
|
+
"""
|
|
113
|
+
cfg_dir = Path(__file__).resolve().parents[1] / "workflow" / "config"
|
|
114
|
+
multi = yaml.safe_load((cfg_dir / "multi.yaml").read_text())
|
|
115
|
+
common = yaml.safe_load((cfg_dir.parent / multi["common"]).read_text())
|
|
116
|
+
paths = [cfg_dir.parent / p for p in multi["segmentations"]]
|
|
117
|
+
cfgs = [{**common, **yaml.safe_load(p.read_text())} for p in paths]
|
|
118
|
+
|
|
119
|
+
assert _validate_configs(paths, cfgs) == common["work_dir"]
|
|
120
|
+
# Every key convert reads comes from the shared file, not a per-config one.
|
|
121
|
+
for path in paths:
|
|
122
|
+
own = yaml.safe_load(path.read_text())
|
|
123
|
+
assert not set(own) & set(_CONVERT_KEYS), path.name
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Settings shared by every segmentation in config/multi.yaml.
|
|
2
|
+
#
|
|
3
|
+
# `common:` in multi.yaml points here, and run_multi passes it as the first of
|
|
4
|
+
# two --configfile values. Snakemake merges them in order with the later
|
|
5
|
+
# winning, so a per-config file only carries what actually differs from this
|
|
6
|
+
# one — the channel it reads, the method it uses, its label_name.
|
|
7
|
+
#
|
|
8
|
+
# Everything `convert` reads MUST live here rather than in a per-config file:
|
|
9
|
+
# the conversion runs once, up front, from the first config only, so a `shard`
|
|
10
|
+
# or `input` set on the second config would be silently ignored. run_multi
|
|
11
|
+
# refuses to start if those keys disagree across configs.
|
|
12
|
+
|
|
13
|
+
input: "/path/to/scan.ims"
|
|
14
|
+
work_dir: "/path/to/results"
|
|
15
|
+
|
|
16
|
+
# --- conversion (read once, in phase A, from the first config only) ---
|
|
17
|
+
reuse_pyramid: false
|
|
18
|
+
convert_chunks: null
|
|
19
|
+
# true → pack chunks into shards: same chunking and same memory, far fewer
|
|
20
|
+
# files. Worth turning on for anything large; convert warns when a store is
|
|
21
|
+
# heading past ~200,000 chunks, which a shared filesystem will not enjoy.
|
|
22
|
+
shard: false
|
|
23
|
+
|
|
24
|
+
# --- label pyramid (read by merge, i.e. once per config) ---
|
|
25
|
+
# Shared here for convenience, but a per-config file may override these: each
|
|
26
|
+
# segmentation builds its own label pyramid.
|
|
27
|
+
pyramid_levels: 5
|
|
28
|
+
pyramid_downscale: 2
|
|
29
|
+
|
|
30
|
+
# --- tiling (must match across configs so label arrays stay comparable) ---
|
|
31
|
+
level: 0
|
|
32
|
+
tile_shape: [16, 1024, 1024]
|
|
33
|
+
gpu_memory_gb: null
|
|
34
|
+
skip_empty: true
|
|
35
|
+
empty_threshold: null
|
|
36
|
+
# Tiles per SLURM job; they run sequentially and share one model load. Raise
|
|
37
|
+
# once you know a tile's runtime -- job wall time is ~N x per-tile time.
|
|
38
|
+
tiles_per_job: 4
|
|
39
|
+
|
|
40
|
+
# --- merge ---
|
|
41
|
+
sequential_labels: true
|
|
42
|
+
merge_workers: null
|
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
# Example segmentation config: cilia channel, via method: "custom" ->
|
|
2
2
|
# patchworks.plugins.dog.segment (deconvolution + difference-of-Gaussians).
|
|
3
3
|
# Cilia are thin/small structures a cell-body model like Cellpose isn't
|
|
4
|
-
# shaped for.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# shaped for. Related to cyto/nuclei via config/multi.yaml to map each
|
|
5
|
+
# cilium to its containing cell / nucleus.
|
|
6
|
+
#
|
|
7
|
+
# Only what differs from config/common.yaml — the input, work_dir, tiling and
|
|
8
|
+
# conversion settings all live there and are merged in ahead of this file.
|
|
8
9
|
#
|
|
9
10
|
# pycudadecon is CUDA-only, so this segment job needs a GPU too — same
|
|
10
11
|
# profile/slurm as the Cellpose configs already covers it.
|
|
11
12
|
|
|
12
|
-
input: "/path/to/scan.ims"
|
|
13
|
-
work_dir: "/path/to/results"
|
|
14
|
-
|
|
15
|
-
reuse_pyramid: false
|
|
16
|
-
convert_chunks: null
|
|
17
|
-
shard: false
|
|
18
|
-
|
|
19
13
|
channel: 2 # cilia marker channel
|
|
20
|
-
|
|
21
|
-
tile_shape: [16, 1024, 1024] # keep identical to config_cyto.yaml/config_nuclei.yaml
|
|
22
|
-
gpu_memory_gb: null
|
|
14
|
+
|
|
23
15
|
# Per-axis halo [z, y, x], covering the PSF support (decon) + the DoG's
|
|
24
16
|
# high_sigma. Lateral 30 px at dxdata 0.1 = 3 um; at dzdata 0.2 that is 15
|
|
25
17
|
# z-planes -- almost the whole 16-plane tile. Deconvolution genuinely wants a
|
|
26
|
-
# deeper z tile than this: if cilia quality matters, raise tile_shape's z
|
|
27
|
-
#
|
|
28
|
-
#
|
|
18
|
+
# deeper z tile than this: if cilia quality matters, raise tile_shape's z in
|
|
19
|
+
# common.yaml (label_relations needs it identical across configs) rather than
|
|
20
|
+
# pushing the halo up against the tile depth.
|
|
29
21
|
overlap: [8, 30, 30]
|
|
30
|
-
skip_empty: true
|
|
31
|
-
# Tiles per SLURM job; they run sequentially and share one decon/GPU context.
|
|
32
|
-
# Raise once you know a tile's runtime -- job wall time is ~N x per-tile time.
|
|
33
|
-
tiles_per_job: 4
|
|
34
|
-
empty_threshold: null
|
|
35
22
|
|
|
36
23
|
method: "custom"
|
|
37
24
|
# dilate: 2 # optional: pixels to grow labels by after segmentation
|
|
@@ -58,8 +45,3 @@ custom:
|
|
|
58
45
|
wavelength: 525
|
|
59
46
|
na: 1.4
|
|
60
47
|
nimm: 1.515
|
|
61
|
-
|
|
62
|
-
pyramid_levels: 5
|
|
63
|
-
pyramid_downscale: 2
|
|
64
|
-
sequential_labels: true
|
|
65
|
-
merge_workers: null
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Example segmentation config: cytoplasm/membrane channel.
|
|
2
|
+
#
|
|
3
|
+
# Only what differs from config/common.yaml — the input, work_dir, tiling and
|
|
4
|
+
# conversion settings all live there and are merged in ahead of this file.
|
|
5
|
+
# Run it via config/multi.yaml, or on its own with both files:
|
|
6
|
+
#
|
|
7
|
+
# snakemake -s Snakefile --configfile config/common.yaml config/config_cyto.yaml
|
|
8
|
+
|
|
9
|
+
channel: 0 # cytoplasm/membrane channel
|
|
10
|
+
|
|
11
|
+
# Per-axis halo [z, y, x]. A scalar 30 would expand a [16, 1024, 1024] tile to
|
|
12
|
+
# 76 x 1084 x 1084 = 5.3x the voxels it keeps, nearly all of it wasted z. The
|
|
13
|
+
# z-halo only needs to cover one cell in z, not one cell in x/y.
|
|
14
|
+
overlap: [4, 30, 30]
|
|
15
|
+
|
|
16
|
+
method: "cellpose"
|
|
17
|
+
label_name: "cyto_labels"
|
|
18
|
+
cellpose:
|
|
19
|
+
model: "cyto3"
|
|
20
|
+
diameter: 30
|
|
21
|
+
do_3D: true
|
|
22
|
+
gpu: true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Example segmentation config: nuclei channel.
|
|
2
|
+
#
|
|
3
|
+
# Only what differs from config/common.yaml — the input, work_dir, tiling and
|
|
4
|
+
# conversion settings all live there and are merged in ahead of this file.
|
|
5
|
+
# Run it via config/multi.yaml, or on its own with both files:
|
|
6
|
+
#
|
|
7
|
+
# snakemake -s Snakefile --configfile config/common.yaml config/config_nuclei.yaml
|
|
8
|
+
|
|
9
|
+
channel: 1 # nuclear stain channel
|
|
10
|
+
|
|
11
|
+
# Per-axis halo [z, y, x]. A scalar 30 would expand a [16, 1024, 1024] tile to
|
|
12
|
+
# 76 x 1084 x 1084 = 5.3x the voxels it keeps, nearly all of it wasted z. The
|
|
13
|
+
# z-halo only needs to cover one nucleus in z, not one nucleus in x/y.
|
|
14
|
+
overlap: [4, 30, 30]
|
|
15
|
+
|
|
16
|
+
method: "cellpose"
|
|
17
|
+
label_name: "nuclei_labels"
|
|
18
|
+
cellpose:
|
|
19
|
+
model: "nuclei"
|
|
20
|
+
diameter: 15
|
|
21
|
+
do_3D: true
|
|
22
|
+
gpu: true
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
# `relations` (there's no automatic "chain"; list every pair explicitly, e.g.
|
|
14
14
|
# nuclei->cyto, nuclei->membrane, cyto->membrane).
|
|
15
15
|
|
|
16
|
+
# Settings every segmentation shares — the input, the work_dir, the tiling,
|
|
17
|
+
# everything `convert` reads. Merged in ahead of each config below, so a
|
|
18
|
+
# per-config file carries only what differs. Optional: drop this line and each
|
|
19
|
+
# config can be self-contained again.
|
|
20
|
+
common: config/common.yaml
|
|
21
|
+
|
|
16
22
|
segmentations:
|
|
17
23
|
- config/config_nuclei.yaml
|
|
18
24
|
- config/config_cyto.yaml
|
|
@@ -49,19 +49,29 @@ def _snakemake_cmd(
|
|
|
49
49
|
targets: list[str] | None = None,
|
|
50
50
|
extra: list[str] | None = None,
|
|
51
51
|
jobname_prefix: str | None = None,
|
|
52
|
+
common: Path | None = None,
|
|
52
53
|
) -> list[str]:
|
|
53
54
|
"""Build one snakemake invocation.
|
|
54
55
|
|
|
55
56
|
Every path is absolutised because ``--directory`` moves the working
|
|
56
57
|
directory: each config needs its own ``.snakemake`` state directory, or
|
|
57
58
|
concurrent runs would contend for the same ``.snakemake/locks/``.
|
|
59
|
+
|
|
60
|
+
*common*, when given, is passed as the first of two ``--configfile``
|
|
61
|
+
values. Snakemake merges them in order with the later winning, so the
|
|
62
|
+
settings every config shares -- the input, the work_dir, everything
|
|
63
|
+
``convert`` reads -- live in one file and the per-config file carries only
|
|
64
|
+
what actually differs.
|
|
58
65
|
"""
|
|
66
|
+
configfiles = [str(configfile.resolve())]
|
|
67
|
+
if common is not None:
|
|
68
|
+
configfiles.insert(0, str(common.resolve()))
|
|
59
69
|
cmd = [
|
|
60
70
|
"snakemake",
|
|
61
71
|
"-s",
|
|
62
72
|
str(workflow_dir / "Snakefile"),
|
|
63
73
|
"--configfile",
|
|
64
|
-
|
|
74
|
+
*configfiles,
|
|
65
75
|
]
|
|
66
76
|
if state_dir is not None:
|
|
67
77
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -112,6 +122,22 @@ def _run(cmd: list[str], workflow_dir: Path) -> int:
|
|
|
112
122
|
return subprocess.run(cmd, cwd=workflow_dir).returncode
|
|
113
123
|
|
|
114
124
|
|
|
125
|
+
# Exactly the config keys scripts/convert.py reads -- keep the two in step.
|
|
126
|
+
# Conversion runs once, in phase A, from the first config, so these have to
|
|
127
|
+
# agree across all of them or the disagreement is invisible.
|
|
128
|
+
#
|
|
129
|
+
# Deliberately NOT here: pyramid_levels / pyramid_downscale. Those are read by
|
|
130
|
+
# merge.py, which runs once per config and builds that config's own label
|
|
131
|
+
# pyramid, so they may legitimately differ.
|
|
132
|
+
_CONVERT_KEYS = (
|
|
133
|
+
"input",
|
|
134
|
+
"sequence_pattern",
|
|
135
|
+
"convert_chunks",
|
|
136
|
+
"shard",
|
|
137
|
+
"reuse_pyramid",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
115
141
|
def _validate_configs(paths: list[Path], cfgs: list[dict]) -> str:
|
|
116
142
|
"""Check the cross-config invariants before anything is submitted.
|
|
117
143
|
|
|
@@ -145,6 +171,20 @@ def _validate_configs(paths: list[Path], cfgs: list[dict]) -> str:
|
|
|
145
171
|
f"share a chunk layout; got {_spread(key)}"
|
|
146
172
|
)
|
|
147
173
|
|
|
174
|
+
# Phase A converts once, from the first config. Anything `convert` reads
|
|
175
|
+
# out of a later config is therefore silently ignored -- someone setting
|
|
176
|
+
# `shard: true` on the second config and watching a million files appear
|
|
177
|
+
# anyway has no way to see why. Refuse instead, and point at common.yaml.
|
|
178
|
+
for key in _CONVERT_KEYS:
|
|
179
|
+
values = {repr(cfg.get(key)) for cfg in cfgs}
|
|
180
|
+
if len(values) != 1:
|
|
181
|
+
problems.append(
|
|
182
|
+
f"{key} affects `convert`, which runs once from the first "
|
|
183
|
+
f"config, so the other values would be silently ignored; got "
|
|
184
|
+
f"{_spread(key)}. Put the settings every config shares in one "
|
|
185
|
+
f"file and point `common:` in multi.yaml at it."
|
|
186
|
+
)
|
|
187
|
+
|
|
148
188
|
for path, cfg in zip(paths, cfgs):
|
|
149
189
|
source = str(cfg.get("input", ""))
|
|
150
190
|
if any(ch in source for ch in "*?[") and not cfg.get(
|
|
@@ -221,7 +261,15 @@ def main() -> None:
|
|
|
221
261
|
seg_config_paths = [
|
|
222
262
|
_resolve(workflow_dir, c) for c in multi_cfg["segmentations"]
|
|
223
263
|
]
|
|
224
|
-
|
|
264
|
+
# Optional shared config: Snakemake merges --configfile values in order,
|
|
265
|
+
# so `common` holds what every segmentation agrees on and each per-config
|
|
266
|
+
# file overrides only what differs. Validation has to see the same merged
|
|
267
|
+
# view Snakemake will, or it would report a missing work_dir that is
|
|
268
|
+
# simply defined one file over.
|
|
269
|
+
common_path = multi_cfg.get("common")
|
|
270
|
+
common_path = _resolve(workflow_dir, common_path) if common_path else None
|
|
271
|
+
common_cfg = _load_yaml(common_path) if common_path else {}
|
|
272
|
+
seg_cfgs = [{**common_cfg, **_load_yaml(p)} for p in seg_config_paths]
|
|
225
273
|
work_dir = _validate_configs(seg_config_paths, seg_cfgs)
|
|
226
274
|
image_store = f"{work_dir}/image.zarr"
|
|
227
275
|
|
|
@@ -245,6 +293,7 @@ def main() -> None:
|
|
|
245
293
|
dry_run=False,
|
|
246
294
|
state_dir=state_dir,
|
|
247
295
|
extra=["--unlock"],
|
|
296
|
+
common=common_path,
|
|
248
297
|
),
|
|
249
298
|
workflow_dir,
|
|
250
299
|
)
|
|
@@ -264,6 +313,7 @@ def main() -> None:
|
|
|
264
313
|
state_dir=Path(work_dir) / ".snakemake_convert",
|
|
265
314
|
targets=[f"{image_store}/zarr.json"],
|
|
266
315
|
jobname_prefix=slurm_jobname_prefix("convert"),
|
|
316
|
+
common=common_path,
|
|
267
317
|
),
|
|
268
318
|
workflow_dir,
|
|
269
319
|
)
|
|
@@ -318,6 +368,7 @@ def main() -> None:
|
|
|
318
368
|
state_dir=Path(cfg["work_dir"]) / cfg["label_name"] / ".snakemake",
|
|
319
369
|
# Names the config in squeue, so concurrent runs are tellable apart.
|
|
320
370
|
jobname_prefix=slurm_jobname_prefix(cfg["label_name"]),
|
|
371
|
+
common=common_path,
|
|
321
372
|
)
|
|
322
373
|
print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
|
|
323
374
|
procs.append((cfg_path.name, subprocess.Popen(cmd, cwd=workflow_dir)))
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Tests for the multi-config driver's SLURM-facing behaviour."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
sys.path.insert(
|
|
8
|
-
0, str(Path(__file__).resolve().parents[1] / "workflow" / "scripts")
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
from run_multi import slurm_jobname_prefix # noqa: E402
|
|
12
|
-
|
|
13
|
-
# The SLURM executor's own rule (snakemake_executor_plugin_slurm): it raises a
|
|
14
|
-
# WorkflowError and aborts the whole run if the prefix does not match.
|
|
15
|
-
_EXECUTOR_RULE = re.compile(r"^[A-Za-z0-9_-]{1,50}$")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_jobname_prefix_satisfies_the_executor():
|
|
19
|
-
"""Whatever a label_name contains, the prefix must stay submittable.
|
|
20
|
-
|
|
21
|
-
The executor names jobs after a UUID and refuses a --job-name override, so
|
|
22
|
-
this prefix is the only thing that makes squeue readable -- and an invalid
|
|
23
|
-
one fails the run rather than degrading.
|
|
24
|
-
"""
|
|
25
|
-
for label in ("nuclei_labels", "cyto_labels", "convert", "a"):
|
|
26
|
-
assert _EXECUTOR_RULE.match(slurm_jobname_prefix(label))
|
|
27
|
-
|
|
28
|
-
# Characters a label might plausibly pick up are sanitised, not passed on.
|
|
29
|
-
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("cilia/v2 (test)"))
|
|
30
|
-
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("run 1: nuclei"))
|
|
31
|
-
# And an over-long label is truncated to the executor's 50-char limit.
|
|
32
|
-
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("x" * 200))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_jobname_prefix_keeps_the_label_readable():
|
|
36
|
-
"""The label must lead, since that is what a queue listing truncates to."""
|
|
37
|
-
assert slurm_jobname_prefix("nuclei_labels") == "pw-nuclei_labels"
|
|
38
|
-
assert slurm_jobname_prefix("convert") == "pw-convert"
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
# Per-axis halo [z, y, x]. A scalar 30 would expand a [16, 1024, 1024] tile to
|
|
19
|
-
# 76 x 1084 x 1084 = 5.3x the voxels it keeps, nearly all of it wasted z. The
|
|
20
|
-
# z-halo only needs to cover one cell in z, not one cell in x/y.
|
|
21
|
-
overlap: [4, 30, 30]
|
|
22
|
-
skip_empty: true
|
|
23
|
-
# Tiles per SLURM job; they run sequentially and share one model load. Raise
|
|
24
|
-
# once you know a tile's runtime -- job wall time is ~N x per-tile time.
|
|
25
|
-
tiles_per_job: 4
|
|
26
|
-
empty_threshold: null
|
|
27
|
-
|
|
28
|
-
method: "cellpose"
|
|
29
|
-
label_name: "cyto_labels"
|
|
30
|
-
cellpose:
|
|
31
|
-
model: "cyto3"
|
|
32
|
-
diameter: 30
|
|
33
|
-
do_3D: true
|
|
34
|
-
gpu: true
|
|
35
|
-
|
|
36
|
-
pyramid_levels: 5
|
|
37
|
-
pyramid_downscale: 2
|
|
38
|
-
sequential_labels: true
|
|
39
|
-
merge_workers: null
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
# Per-axis halo [z, y, x]. A scalar 30 would expand a [16, 1024, 1024] tile to
|
|
19
|
-
# 76 x 1084 x 1084 = 5.3x the voxels it keeps, nearly all of it wasted z. The
|
|
20
|
-
# z-halo only needs to cover one nucleus in z, not one nucleus in x/y.
|
|
21
|
-
overlap: [4, 30, 30]
|
|
22
|
-
skip_empty: true
|
|
23
|
-
# Tiles per SLURM job; they run sequentially and share one model load. Raise
|
|
24
|
-
# once you know a tile's runtime -- job wall time is ~N x per-tile time.
|
|
25
|
-
tiles_per_job: 4
|
|
26
|
-
empty_threshold: null
|
|
27
|
-
|
|
28
|
-
method: "cellpose"
|
|
29
|
-
label_name: "nuclei_labels"
|
|
30
|
-
cellpose:
|
|
31
|
-
model: "nuclei"
|
|
32
|
-
diameter: 15
|
|
33
|
-
do_3D: true
|
|
34
|
-
gpu: true
|
|
35
|
-
|
|
36
|
-
pyramid_levels: 5
|
|
37
|
-
pyramid_downscale: 2
|
|
38
|
-
sequential_labels: true
|
|
39
|
-
merge_workers: null
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|