patchworks 2.1.2__tar.gz → 2.2.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.1.2 → patchworks-2.2.0}/PKG-INFO +1 -1
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/label_relations.md +2 -2
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/measurements.md +1 -1
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/snakemake.md +21 -0
- patchworks-2.2.0/tests/test_run_multi.py +38 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/profile/slurm/config.yaml +8 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/run_multi.py +31 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/.github/workflows/docs.yml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/.github/workflows/lint.yml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/.github/workflows/release.yml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/.gitignore +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/LICENSE +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/README.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/cliff.toml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/chunks.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/cluster.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/io.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/postprocess.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/relabel.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/api/tile_process.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/assets/logo.png +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/custom.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/custom_method.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/dog.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/dog.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/stardist.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/getting_started.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/merging.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/performance.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/guide/tiling.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/docs/index.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/mkdocs.yml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/pyproject.toml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/__init__.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_core.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_io.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_merge.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/_relations.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_allocation.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_core.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_distributed.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_dog.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_gpu.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_napari.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_occupancy.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_postprocess.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/tests/test_relations.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/README.md +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/Snakefile +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/config/config.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/config/multi.yaml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/pixi.toml +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/rules/common.smk +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/rules/convert.smk +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/rules/merge.smk +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/rules/segment.smk +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/convert.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/merge.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.0}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-2.1.2 → patchworks-2.2.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.2.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
|
|
@@ -24,7 +24,7 @@ table[2]
|
|
|
24
24
|
`table` only contains matched `a` labels (nuclei with at least one
|
|
25
25
|
overlapping voxel in `cells`) — unmatched labels and full per-`b` coverage
|
|
26
26
|
need a bit more bookkeeping (the [cluster workflow's `run_multi.py`
|
|
27
|
-
script](snakemake.md#one-command-multiple-segmentations
|
|
27
|
+
script](snakemake.md#one-command-multiple-segmentations-relations) does
|
|
28
28
|
this for you and writes it as a two-sheet workbook).
|
|
29
29
|
|
|
30
30
|
Save it as a table yourself:
|
|
@@ -41,4 +41,4 @@ with open("nuclei_to_cell.csv", "w", newline="") as f:
|
|
|
41
41
|
|
|
42
42
|
On the cluster, producing the two label stores in the first place is a
|
|
43
43
|
matter of running the workflow twice against the same `work_dir` — see
|
|
44
|
-
[Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei
|
|
44
|
+
[Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei-cytoplasm).
|
|
@@ -10,7 +10,7 @@ is built for this — its "Measure" dock widget computes area/centroid/intensity
|
|
|
10
10
|
stats directly off a Labels layer's dask/zarr-backed array, out-of-core, and
|
|
11
11
|
scales with chunk count rather than object count. It's the best fit for
|
|
12
12
|
measuring *every* object in a store this size, not just a cropped region —
|
|
13
|
-
see [View image + labels in napari](ome_zarr_napari.md#view-image
|
|
13
|
+
see [View image + labels in napari](ome_zarr_napari.md#view-image-labels-in-napari).
|
|
14
14
|
Bundled in `patchworks[napari]`.
|
|
15
15
|
|
|
16
16
|
For interactively inspecting individual cells by clicking in the viewer (not
|
|
@@ -187,6 +187,27 @@ Snakemake submits `convert`, then `prepare`, then **one `segment` job per
|
|
|
187
187
|
batch of `tiles_per_job` non-empty tiles** (up to `jobs:` at once → that many
|
|
188
188
|
GPUs in parallel), then `merge`. Raise `jobs:` to use more GPUs.
|
|
189
189
|
|
|
190
|
+
!!! tip "Recognisable job names in `squeue`"
|
|
191
|
+
The SLURM executor names every job after its run UUID and **rejects** a
|
|
192
|
+
`--job-name` in `slurm_extra`, so by default `squeue` shows nothing you
|
|
193
|
+
can identify. A prefix is the supported lever, and it goes first in the
|
|
194
|
+
name (`<prefix>_<uuid>`) — the part a queue listing truncates to:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
slurm-jobname-prefix: patchworks # already in the shipped profile
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`run_multi` overrides it per config, so a three-way run shows
|
|
201
|
+
`pw-convert`, then `pw-nuclei_labels` / `pw-cyto_labels` /
|
|
202
|
+
`pw-cilia_labels` — telling the concurrent runs apart at a glance:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
squeue -u $USER -o '%.18i %.24j %.8T %.10M'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Alphanumerics, underscores and hyphens only, 50 characters max; an
|
|
209
|
+
invalid prefix fails the run, so `label_name` is sanitised before use.
|
|
210
|
+
|
|
190
211
|
!!! tip "Sizing memory"
|
|
191
212
|
Every step now sizes its own worker counts from what SLURM actually
|
|
192
213
|
granted (`SLURM_CPUS_PER_TASK`, `SLURM_MEM_PER_*`, the cgroup limit)
|
|
@@ -0,0 +1,38 @@
|
|
|
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"
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
# Edit the partitions / account / GPU request for your cluster.
|
|
6
6
|
|
|
7
7
|
executor: slurm
|
|
8
|
+
# The SLURM executor names every job after its run UUID and rejects a
|
|
9
|
+
# --job-name in slurm_extra, so `squeue` shows nothing recognisable without
|
|
10
|
+
# this. The prefix goes first in the name (<prefix>_<uuid>), which is the part
|
|
11
|
+
# that survives truncation in a queue listing.
|
|
12
|
+
# Alphanumerics, underscores and hyphens only; 50 characters max.
|
|
13
|
+
# run_multi overrides this per config, so a three-way run shows pw-convert,
|
|
14
|
+
# then pw-nuclei_labels / pw-cyto_labels / pw-cilia_labels.
|
|
15
|
+
slurm-jobname-prefix: patchworks
|
|
8
16
|
# Max concurrent SLURM jobs Snakemake will have in flight at once. This is a
|
|
9
17
|
# submission-side cap, not a cluster limit — raising it just lets more
|
|
10
18
|
# "segment" tiles queue/run together; scicore's scheduler still enforces your
|
|
@@ -26,6 +26,7 @@ overlap, including b-objects with zero matches).
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
28
|
import argparse
|
|
29
|
+
import re
|
|
29
30
|
import subprocess
|
|
30
31
|
import sys
|
|
31
32
|
from pathlib import Path
|
|
@@ -47,6 +48,7 @@ def _snakemake_cmd(
|
|
|
47
48
|
state_dir: Path | None = None,
|
|
48
49
|
targets: list[str] | None = None,
|
|
49
50
|
extra: list[str] | None = None,
|
|
51
|
+
jobname_prefix: str | None = None,
|
|
50
52
|
) -> list[str]:
|
|
51
53
|
"""Build one snakemake invocation.
|
|
52
54
|
|
|
@@ -66,6 +68,9 @@ def _snakemake_cmd(
|
|
|
66
68
|
cmd += ["--directory", str(state_dir.resolve())]
|
|
67
69
|
if profile:
|
|
68
70
|
cmd += ["--workflow-profile", str((workflow_dir / profile).resolve())]
|
|
71
|
+
if jobname_prefix:
|
|
72
|
+
# A SLURM-executor setting, so only valid alongside the profile.
|
|
73
|
+
cmd += ["--slurm-jobname-prefix", jobname_prefix]
|
|
69
74
|
else:
|
|
70
75
|
cmd += ["--cores", str(cores), "--rerun-triggers", "mtime"]
|
|
71
76
|
if dry_run:
|
|
@@ -79,6 +84,29 @@ def _snakemake_cmd(
|
|
|
79
84
|
return cmd
|
|
80
85
|
|
|
81
86
|
|
|
87
|
+
def slurm_jobname_prefix(label: str) -> str:
|
|
88
|
+
"""Sanitise *label* into a SLURM job-name prefix the executor accepts.
|
|
89
|
+
|
|
90
|
+
The SLURM executor names every job after its run UUID and refuses a
|
|
91
|
+
``--job-name`` in ``slurm_extra``, so a prefix is the only way to get
|
|
92
|
+
something recognisable into ``squeue``. It becomes ``<prefix>_<uuid>``,
|
|
93
|
+
which puts the readable part first -- the part that survives truncation
|
|
94
|
+
in a queue listing.
|
|
95
|
+
|
|
96
|
+
The executor requires alphanumerics, underscores and hyphens only, at
|
|
97
|
+
most 50 characters, and rejects the whole run otherwise.
|
|
98
|
+
|
|
99
|
+
Examples
|
|
100
|
+
--------
|
|
101
|
+
>>> slurm_jobname_prefix("nuclei_labels")
|
|
102
|
+
'pw-nuclei_labels'
|
|
103
|
+
>>> slurm_jobname_prefix("cilia/v2 (test)")
|
|
104
|
+
'pw-cilia-v2--test-'
|
|
105
|
+
"""
|
|
106
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "-", label)
|
|
107
|
+
return f"pw-{safe}"[:50]
|
|
108
|
+
|
|
109
|
+
|
|
82
110
|
def _run(cmd: list[str], workflow_dir: Path) -> int:
|
|
83
111
|
print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
|
|
84
112
|
return subprocess.run(cmd, cwd=workflow_dir).returncode
|
|
@@ -235,6 +263,7 @@ def main() -> None:
|
|
|
235
263
|
dry_run=args.dry_run,
|
|
236
264
|
state_dir=Path(work_dir) / ".snakemake_convert",
|
|
237
265
|
targets=[f"{image_store}/zarr.json"],
|
|
266
|
+
jobname_prefix=slurm_jobname_prefix("convert"),
|
|
238
267
|
),
|
|
239
268
|
workflow_dir,
|
|
240
269
|
)
|
|
@@ -287,6 +316,8 @@ def main() -> None:
|
|
|
287
316
|
cores=args.cores,
|
|
288
317
|
dry_run=args.dry_run,
|
|
289
318
|
state_dir=Path(cfg["work_dir"]) / cfg["label_name"] / ".snakemake",
|
|
319
|
+
# Names the config in squeue, so concurrent runs are tellable apart.
|
|
320
|
+
jobname_prefix=slurm_jobname_prefix(cfg["label_name"]),
|
|
290
321
|
)
|
|
291
322
|
print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
|
|
292
323
|
procs.append((cfg_path.name, subprocess.Popen(cmd, cwd=workflow_dir)))
|
|
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
|
|
File without changes
|
|
File without changes
|