traceratops 2.0.0__py3-none-any.whl
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.
- traceratops/__init__.py +0 -0
- traceratops/_version.py +21 -0
- traceratops/collect_files.py +388 -0
- traceratops/core/build_matrix.py +518 -0
- traceratops/core/chromatin_trace_table.py +1385 -0
- traceratops/core/him_matrix_operations.py +1508 -0
- traceratops/core/io_manager.py +66 -0
- traceratops/core/localization_table.py +774 -0
- traceratops/core/plotting_functions.py +404 -0
- traceratops/core/trace_resolver/__init__.py +37 -0
- traceratops/core/trace_resolver/beam.py +120 -0
- traceratops/core/trace_resolver/classifier.py +403 -0
- traceratops/core/trace_resolver/model.py +151 -0
- traceratops/core/trace_resolver/resolver.py +206 -0
- traceratops/core/trace_resolver/results.py +73 -0
- traceratops/core/trace_resolver/scoring.py +48 -0
- traceratops/localization_analyzer.py +88 -0
- traceratops/localization_merge.py +123 -0
- traceratops/plot_3way_coloc.py +216 -0
- traceratops/plot_4m.py +272 -0
- traceratops/plot_bootstrapping.py +267 -0
- traceratops/plot_compare2matrices.py +334 -0
- traceratops/plot_him_matrix.py +321 -0
- traceratops/plot_matrix_comparison.py +285 -0
- traceratops/pymol_script.py +144 -0
- traceratops/script_banner.py +56 -0
- traceratops/trace_3way_coloc.py +509 -0
- traceratops/trace_analyzer.py +776 -0
- traceratops/trace_assign_mask.py +249 -0
- traceratops/trace_export_to_fofct.py +281 -0
- traceratops/trace_filter.py +452 -0
- traceratops/trace_genomic_coordinates.py +216 -0
- traceratops/trace_import_from_fofct.py +187 -0
- traceratops/trace_merge.py +138 -0
- traceratops/trace_pearsons.py +393 -0
- traceratops/trace_physical_vs_genomic_distance.py +452 -0
- traceratops/trace_plot.py +226 -0
- traceratops/trace_sorter.py +107 -0
- traceratops/trace_split_labels.py +132 -0
- traceratops/trace_splitter.py +851 -0
- traceratops/trace_stats.py +83 -0
- traceratops/trace_to_matrix.py +159 -0
- traceratops-2.0.0.dist-info/METADATA +799 -0
- traceratops-2.0.0.dist-info/RECORD +48 -0
- traceratops-2.0.0.dist-info/WHEEL +5 -0
- traceratops-2.0.0.dist-info/entry_points.txt +30 -0
- traceratops-2.0.0.dist-info/licenses/LICENSE +674 -0
- traceratops-2.0.0.dist-info/top_level.txt +1 -0
traceratops/__init__.py
ADDED
|
File without changes
|
traceratops/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
New release versions are made using [Semantic Versioning](https://semver.org/).
|
|
3
|
+
|
|
4
|
+
> Given a version number MAJOR.MINOR.PATCH, increment the:
|
|
5
|
+
|
|
6
|
+
1. MAJOR version when you make incompatible API changes,
|
|
7
|
+
2. MINOR version when you add functionality in a backwards compatible manner, and
|
|
8
|
+
3. PATCH version when you make backwards compatible bug fixes.
|
|
9
|
+
|
|
10
|
+
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
"0.0.0.dev0"
|
|
14
|
+
"1.2.34.dev0"
|
|
15
|
+
"1.2.34a0"
|
|
16
|
+
"1.2.34"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
|
|
21
|
+
__version__ = importlib.metadata.version("traceratops")
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Collect one file per subdirectory using pattern matching.
|
|
3
|
+
|
|
4
|
+
Walks the immediate subdirectories of a root folder, searches recursively
|
|
5
|
+
for files whose name matches an example, and copies exactly one match per
|
|
6
|
+
subdirectory to a destination folder.
|
|
7
|
+
|
|
8
|
+
Two matching modes:
|
|
9
|
+
|
|
10
|
+
**Exact match** (no ``--variable-part``)::
|
|
11
|
+
|
|
12
|
+
collect_files --root data/RUT \\
|
|
13
|
+
--example-file "localizations_3D_barcode.ecsv" \\
|
|
14
|
+
--copy-to collected/
|
|
15
|
+
|
|
16
|
+
Files are automatically renamed with the subdirectory name
|
|
17
|
+
(``localizations_3D_barcode_013_ROI.ecsv``) since they all share the
|
|
18
|
+
same name.
|
|
19
|
+
|
|
20
|
+
**Variable match** (with ``--variable-part``)::
|
|
21
|
+
|
|
22
|
+
collect_files --root data/RUT \\
|
|
23
|
+
--example-file "Trace_3D_barcode_mask-mask0_ROI-13.ecsv" \\
|
|
24
|
+
--variable-part "13" \\
|
|
25
|
+
--copy-to collected/
|
|
26
|
+
|
|
27
|
+
Matches ``ROI-14.ecsv`` but rejects ``ROI-021.ecsv`` (different length).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import shutil
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from traceratops.script_banner import print_script_banner
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Business logic
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class FilePattern:
|
|
47
|
+
"""Filename pattern with an optional fixed-length variable zone.
|
|
48
|
+
|
|
49
|
+
When *variable_length* is 0, the pattern performs exact matching
|
|
50
|
+
(the candidate must equal *prefix*). Otherwise a candidate matches
|
|
51
|
+
when its total length equals ``len(prefix) + variable_length +
|
|
52
|
+
len(suffix)`` and the prefix and suffix are identical.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
prefix: str
|
|
56
|
+
suffix: str
|
|
57
|
+
variable_length: int
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def total_length(self) -> int:
|
|
61
|
+
return len(self.prefix) + self.variable_length + len(self.suffix)
|
|
62
|
+
|
|
63
|
+
def matches(self, filename: str) -> bool:
|
|
64
|
+
"""Return True if *filename* matches this pattern."""
|
|
65
|
+
return (
|
|
66
|
+
len(filename) == self.total_length
|
|
67
|
+
and filename[: len(self.prefix)] == self.prefix
|
|
68
|
+
and filename[len(self.prefix) + self.variable_length :] == self.suffix
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ScanResult:
|
|
74
|
+
"""Outcome of scanning subdirectories for pattern matches."""
|
|
75
|
+
|
|
76
|
+
unique: dict[str, Path] = field(default_factory=dict)
|
|
77
|
+
ambiguous: dict[str, list[Path]] = field(default_factory=dict)
|
|
78
|
+
empty: list[str] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_pattern(example_file: str, variable_part: str | None = None) -> FilePattern:
|
|
82
|
+
"""Derive a :class:`FilePattern` from *example_file*.
|
|
83
|
+
|
|
84
|
+
When *variable_part* is ``None``, the pattern matches the exact
|
|
85
|
+
filename. Otherwise it matches filenames that are identical to
|
|
86
|
+
*example_file* except at the position of *variable_part*, where any
|
|
87
|
+
characters of the same length are accepted.
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
------
|
|
91
|
+
ValueError
|
|
92
|
+
If *variable_part* is the empty string, or appears zero or more
|
|
93
|
+
than one time in *example_file*.
|
|
94
|
+
"""
|
|
95
|
+
if variable_part is None:
|
|
96
|
+
return FilePattern(prefix=example_file, suffix="", variable_length=0)
|
|
97
|
+
|
|
98
|
+
if not variable_part:
|
|
99
|
+
raise ValueError("variable_part must not be empty")
|
|
100
|
+
|
|
101
|
+
count = example_file.count(variable_part)
|
|
102
|
+
if count == 0:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Variable part {variable_part!r} not found in "
|
|
105
|
+
f"example file {example_file!r}"
|
|
106
|
+
)
|
|
107
|
+
if count > 1:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Variable part {variable_part!r} appears {count} times in "
|
|
110
|
+
f"example file {example_file!r} (must appear exactly once)"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
pos = example_file.index(variable_part)
|
|
114
|
+
return FilePattern(
|
|
115
|
+
prefix=example_file[:pos],
|
|
116
|
+
suffix=example_file[pos + len(variable_part) :],
|
|
117
|
+
variable_length=len(variable_part),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def scan_subdirectories(root: Path, pattern: FilePattern) -> ScanResult:
|
|
122
|
+
"""Scan immediate subdirectories of *root* for files matching *pattern*.
|
|
123
|
+
|
|
124
|
+
Each immediate child directory of *root* is an independent collection
|
|
125
|
+
unit. Files are searched **recursively** within each subdirectory.
|
|
126
|
+
"""
|
|
127
|
+
result = ScanResult()
|
|
128
|
+
|
|
129
|
+
subdirs = sorted(d for d in root.iterdir() if d.is_dir())
|
|
130
|
+
for subdir in subdirs:
|
|
131
|
+
matches = sorted(
|
|
132
|
+
f for f in subdir.rglob("*") if f.is_file() and pattern.matches(f.name)
|
|
133
|
+
)
|
|
134
|
+
if len(matches) == 0:
|
|
135
|
+
result.empty.append(subdir.name)
|
|
136
|
+
elif len(matches) == 1:
|
|
137
|
+
result.unique[subdir.name] = matches[0]
|
|
138
|
+
else:
|
|
139
|
+
result.ambiguous[subdir.name] = matches
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def dest_name(source: Path, subdir_name: str, rename: bool) -> str:
|
|
145
|
+
"""Compute the destination filename for a collected file.
|
|
146
|
+
|
|
147
|
+
When *rename* is True, the subdirectory name is inserted before the
|
|
148
|
+
extension: ``data.ecsv`` from ``013_ROI`` becomes ``data_013_ROI.ecsv``.
|
|
149
|
+
"""
|
|
150
|
+
if not rename:
|
|
151
|
+
return source.name
|
|
152
|
+
return f"{source.stem}_{subdir_name}{source.suffix}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_collisions(
|
|
156
|
+
files: dict[str, Path], copy_to: Path, rename: bool = False
|
|
157
|
+
) -> list[str]:
|
|
158
|
+
"""Return human-readable collision descriptions, if any.
|
|
159
|
+
|
|
160
|
+
Checks for:
|
|
161
|
+
* collisions between collected files (same destination name)
|
|
162
|
+
* collisions with files already present in *copy_to*
|
|
163
|
+
"""
|
|
164
|
+
problems: list[str] = []
|
|
165
|
+
seen: dict[str, str] = {} # dest filename -> subdir name
|
|
166
|
+
|
|
167
|
+
for subdir_name, path in sorted(files.items()):
|
|
168
|
+
name = dest_name(path, subdir_name, rename)
|
|
169
|
+
if name in seen:
|
|
170
|
+
problems.append(
|
|
171
|
+
f" {name} from {subdir_name!r} collides with "
|
|
172
|
+
f"file from {seen[name]!r}"
|
|
173
|
+
)
|
|
174
|
+
seen[name] = subdir_name
|
|
175
|
+
|
|
176
|
+
if (copy_to / name).exists():
|
|
177
|
+
problems.append(
|
|
178
|
+
f" {name} from {subdir_name!r} collides with "
|
|
179
|
+
f"existing file in {copy_to}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return problems
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def format_report(result: ScanResult) -> str:
|
|
186
|
+
"""Build a deterministic, sorted, human-readable scan report."""
|
|
187
|
+
lines: list[str] = []
|
|
188
|
+
|
|
189
|
+
if result.unique:
|
|
190
|
+
lines.append(f"Matched ({len(result.unique)}):")
|
|
191
|
+
for name in sorted(result.unique):
|
|
192
|
+
lines.append(f" {name} -> {result.unique[name]}")
|
|
193
|
+
|
|
194
|
+
if result.empty:
|
|
195
|
+
lines.append(f"No match ({len(result.empty)}):")
|
|
196
|
+
for name in result.empty:
|
|
197
|
+
lines.append(f" {name}")
|
|
198
|
+
|
|
199
|
+
if result.ambiguous:
|
|
200
|
+
lines.append(f"Ambiguous ({len(result.ambiguous)}):")
|
|
201
|
+
for name in sorted(result.ambiguous):
|
|
202
|
+
paths = result.ambiguous[name]
|
|
203
|
+
lines.append(f" {name} ({len(paths)} matches):")
|
|
204
|
+
for p in paths:
|
|
205
|
+
lines.append(f" {p}")
|
|
206
|
+
|
|
207
|
+
return "\n".join(lines)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def collect_files(
|
|
211
|
+
root: Path,
|
|
212
|
+
example_file: str,
|
|
213
|
+
copy_to: Path,
|
|
214
|
+
variable_part: str | None = None,
|
|
215
|
+
force: bool = False,
|
|
216
|
+
rename: bool = False,
|
|
217
|
+
) -> list[Path]:
|
|
218
|
+
"""Collect one file per subdirectory and copy to *copy_to*.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
root : Path
|
|
223
|
+
Root directory whose immediate children are the collection units.
|
|
224
|
+
example_file : str
|
|
225
|
+
Example filename. When *variable_part* is given it must contain
|
|
226
|
+
exactly one occurrence of it; otherwise an exact match is used.
|
|
227
|
+
copy_to : Path
|
|
228
|
+
Destination folder (created if missing).
|
|
229
|
+
variable_part : str or None
|
|
230
|
+
The substring that varies across subdirectories. When ``None``,
|
|
231
|
+
exact matching is used and files are automatically renamed with
|
|
232
|
+
the subdirectory name to avoid collisions.
|
|
233
|
+
force : bool
|
|
234
|
+
When True, subdirectories with no match are silently skipped.
|
|
235
|
+
rename : bool
|
|
236
|
+
When True, the subdirectory name is inserted before the file
|
|
237
|
+
extension (e.g. ``data.dat`` from ``013_ROI`` becomes
|
|
238
|
+
``data_013_ROI.dat``). Automatically enabled when
|
|
239
|
+
*variable_part* is ``None``.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
list[Path]
|
|
244
|
+
Paths of the copied files in *copy_to*.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
ValueError
|
|
249
|
+
Bad arguments or ambiguous matches.
|
|
250
|
+
FileNotFoundError
|
|
251
|
+
Empty subdirectories without *force*, or nothing to copy.
|
|
252
|
+
FileExistsError
|
|
253
|
+
Name collisions in *copy_to*.
|
|
254
|
+
"""
|
|
255
|
+
# Exact match → rename is mandatory (all files share the same name).
|
|
256
|
+
if variable_part is None:
|
|
257
|
+
rename = True
|
|
258
|
+
|
|
259
|
+
pattern = build_pattern(example_file, variable_part)
|
|
260
|
+
result = scan_subdirectories(root, pattern)
|
|
261
|
+
|
|
262
|
+
report = format_report(result)
|
|
263
|
+
if report:
|
|
264
|
+
print(report)
|
|
265
|
+
|
|
266
|
+
if result.ambiguous:
|
|
267
|
+
print("\nDecision: nothing copied.")
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"{len(result.ambiguous)} subdirectory(ies) have multiple matches. "
|
|
270
|
+
"Use a more specific --variable-part to disambiguate."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if result.empty and not force:
|
|
274
|
+
print("\nDecision: nothing copied.")
|
|
275
|
+
raise FileNotFoundError(
|
|
276
|
+
f"{len(result.empty)} subdirectory(ies) have no matching files. "
|
|
277
|
+
"Use --force to skip them."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if not result.unique:
|
|
281
|
+
print("\nDecision: nothing copied.")
|
|
282
|
+
raise FileNotFoundError("No files found to copy.")
|
|
283
|
+
|
|
284
|
+
copy_to.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
|
|
286
|
+
collisions = check_collisions(result.unique, copy_to, rename)
|
|
287
|
+
if collisions:
|
|
288
|
+
print("\nDecision: nothing copied.")
|
|
289
|
+
raise FileExistsError(
|
|
290
|
+
"Name collisions in destination:\n" + "\n".join(collisions)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
copied: list[Path] = []
|
|
294
|
+
for subdir_name, source in sorted(result.unique.items()):
|
|
295
|
+
dest = copy_to / dest_name(source, subdir_name, rename)
|
|
296
|
+
shutil.copy2(source, dest)
|
|
297
|
+
copied.append(dest)
|
|
298
|
+
|
|
299
|
+
print(f"\nCopied {len(copied)} file(s) to {copy_to}")
|
|
300
|
+
return copied
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# CLI layer
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def parse_arguments() -> argparse.ArgumentParser:
|
|
309
|
+
"""Build and return the argument parser (used by sphinx-argparse)."""
|
|
310
|
+
parser = argparse.ArgumentParser(
|
|
311
|
+
prog="collect_files",
|
|
312
|
+
description=(
|
|
313
|
+
"Collect exactly one file per subdirectory of ROOT by matching "
|
|
314
|
+
"EXAMPLE_FILE. Without --variable-part the match is exact and "
|
|
315
|
+
"files are automatically renamed with the subdirectory name. "
|
|
316
|
+
"With --variable-part a fixed-length pattern match is used."
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
parser.add_argument(
|
|
320
|
+
"--root",
|
|
321
|
+
type=Path,
|
|
322
|
+
required=True,
|
|
323
|
+
help="Root directory to scan (immediate subdirectories are collection units)",
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument(
|
|
326
|
+
"--example-file",
|
|
327
|
+
required=True,
|
|
328
|
+
help="Example filename to search for in each subdirectory",
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument(
|
|
331
|
+
"--variable-part",
|
|
332
|
+
default=None,
|
|
333
|
+
help=(
|
|
334
|
+
"Substring in EXAMPLE_FILE that varies across subdirectories. "
|
|
335
|
+
"When omitted, exact filename matching is used and files are "
|
|
336
|
+
"automatically renamed with the subdirectory name."
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--copy-to",
|
|
341
|
+
type=Path,
|
|
342
|
+
required=True,
|
|
343
|
+
help="Destination folder for collected files (created if missing)",
|
|
344
|
+
)
|
|
345
|
+
parser.add_argument(
|
|
346
|
+
"--force",
|
|
347
|
+
action="store_true",
|
|
348
|
+
help="Skip subdirectories with no match instead of failing",
|
|
349
|
+
)
|
|
350
|
+
parser.add_argument(
|
|
351
|
+
"--rename",
|
|
352
|
+
action="store_true",
|
|
353
|
+
help=(
|
|
354
|
+
"Rename collected files by inserting the subdirectory name "
|
|
355
|
+
"before the extension (e.g. data.dat from 013_ROI -> "
|
|
356
|
+
"data_013_ROI.dat). Automatic when --variable-part is omitted."
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
return parser
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv: list[str] | None = None) -> int:
|
|
363
|
+
"""Entry point. Returns 0 on success, 1 on business-logic failure."""
|
|
364
|
+
print_script_banner(__file__, __doc__)
|
|
365
|
+
args = parse_arguments().parse_args(argv)
|
|
366
|
+
|
|
367
|
+
if not args.root.is_dir():
|
|
368
|
+
print(f"Error: {args.root} is not a directory", file=sys.stderr)
|
|
369
|
+
return 1
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
collect_files(
|
|
373
|
+
root=args.root,
|
|
374
|
+
example_file=args.example_file,
|
|
375
|
+
copy_to=args.copy_to,
|
|
376
|
+
variable_part=args.variable_part,
|
|
377
|
+
force=args.force,
|
|
378
|
+
rename=args.rename,
|
|
379
|
+
)
|
|
380
|
+
except (ValueError, FileNotFoundError, FileExistsError) as exc:
|
|
381
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
382
|
+
return 1
|
|
383
|
+
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
sys.exit(main())
|