ribometric 1.5.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.
- RiboMetric/RiboMetric.py +634 -0
- RiboMetric/__init__.py +5 -0
- RiboMetric/arg_parser.py +434 -0
- RiboMetric/bam_processing.py +853 -0
- RiboMetric/cli.py +46 -0
- RiboMetric/config.yml +360 -0
- RiboMetric/evaluate.py +139 -0
- RiboMetric/file_parser.py +610 -0
- RiboMetric/file_splitting.py +255 -0
- RiboMetric/html_report.py +478 -0
- RiboMetric/metrics.py +1147 -0
- RiboMetric/modules.py +1729 -0
- RiboMetric/plots.py +1310 -0
- RiboMetric/qc.py +1244 -0
- RiboMetric/registry.py +461 -0
- RiboMetric/results_output.py +923 -0
- RiboMetric/rust.py +524 -0
- RiboMetric/scoring.py +377 -0
- RiboMetric/templates/RiboMetric_favicon.png +0 -0
- RiboMetric/templates/RiboMetric_logo.png +0 -0
- RiboMetric/templates/__init__.py +1 -0
- RiboMetric/templates/base.html +412 -0
- RiboMetric/tui.py +572 -0
- ribometric-1.5.0.dist-info/METADATA +333 -0
- ribometric-1.5.0.dist-info/RECORD +30 -0
- ribometric-1.5.0.dist-info/WHEEL +5 -0
- ribometric-1.5.0.dist-info/entry_points.txt +3 -0
- ribometric-1.5.0.dist-info/licenses/AUTHORS.rst +13 -0
- ribometric-1.5.0.dist-info/licenses/LICENSE +22 -0
- ribometric-1.5.0.dist-info/top_level.txt +1 -0
RiboMetric/RiboMetric.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main module for RiboMetric
|
|
3
|
+
Handles the command line interface and calls the appropriate functions
|
|
4
|
+
|
|
5
|
+
Many different input combinations are possible.
|
|
6
|
+
|
|
7
|
+
Minimal Set:
|
|
8
|
+
-b, --bam <path> : Path to the bam file
|
|
9
|
+
|
|
10
|
+
With this set the calculations will potentially less reliable and no gene
|
|
11
|
+
feature information will be included in the output
|
|
12
|
+
|
|
13
|
+
Standard Set:
|
|
14
|
+
-b, --bam <path> : Path to the bam file
|
|
15
|
+
-g, --gff <path> : Path to the gff file
|
|
16
|
+
|
|
17
|
+
with this set the calculations will be more reliable and gene feature
|
|
18
|
+
information will be included in the output
|
|
19
|
+
|
|
20
|
+
Full Set:
|
|
21
|
+
-b, --bam <path> : Path to the bam file
|
|
22
|
+
-g, --gff <path> : Path to the gff file
|
|
23
|
+
-t, --transcriptome <path> : Path to the transcriptome fasta file
|
|
24
|
+
|
|
25
|
+
with this set the calculations will contain the post information in its
|
|
26
|
+
output but will take longest to run
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Optional Arguments:
|
|
30
|
+
-n, --name <str> : Name of the sample being analysed
|
|
31
|
+
(default: filename of bam file)
|
|
32
|
+
-S, --subsample <int> : Number of reads to subsample from the bam file
|
|
33
|
+
(default: 10000000)
|
|
34
|
+
-T, --transcripts <int> : Number of transcripts to consider
|
|
35
|
+
(default: 100000)
|
|
36
|
+
-c, --config <path> : Path to the config file
|
|
37
|
+
(default: config.yaml)
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
--json : Output the results as a json file
|
|
41
|
+
--html : Output the results as an html file (default)
|
|
42
|
+
--pdf : Output the results as a pdf file
|
|
43
|
+
--csv : Output the results as a csv file
|
|
44
|
+
--all : Output the results as all of the above
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import argparse
|
|
48
|
+
import hashlib
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import platform
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any, Dict, Optional
|
|
55
|
+
|
|
56
|
+
from rich.console import Console
|
|
57
|
+
from rich.table import Table
|
|
58
|
+
from rich.text import Text
|
|
59
|
+
|
|
60
|
+
from .arg_parser import argument_parser, open_config
|
|
61
|
+
from .bam_processing import recompute_sequence_summaries
|
|
62
|
+
from .file_parser import (
|
|
63
|
+
check_annotation,
|
|
64
|
+
check_bam,
|
|
65
|
+
deterministic_subsample,
|
|
66
|
+
flagstat_bam,
|
|
67
|
+
parse_annotation,
|
|
68
|
+
parse_bam,
|
|
69
|
+
parse_fasta,
|
|
70
|
+
prepare_annotation,
|
|
71
|
+
)
|
|
72
|
+
from .html_report import generate_report, parse_json_input
|
|
73
|
+
from .plots import generate_plots
|
|
74
|
+
from .qc import annotation_mode
|
|
75
|
+
from .results_output import (
|
|
76
|
+
generate_all_outputs,
|
|
77
|
+
generate_comparison_ready_csv,
|
|
78
|
+
generate_csv,
|
|
79
|
+
generate_json,
|
|
80
|
+
generate_metrics_table_csv,
|
|
81
|
+
generate_offsets_tsv,
|
|
82
|
+
generate_qc_status,
|
|
83
|
+
generate_summary_tsv,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_HASH_SAMPLE_BYTES = 1024 * 1024
|
|
87
|
+
_FULL_HASH_LIMIT_BYTES = 50 * 1024 * 1024
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _config_path_used(args: argparse.Namespace) -> str:
|
|
91
|
+
if os.path.exists(args.config):
|
|
92
|
+
return str(args.config)
|
|
93
|
+
return str(Path(__file__).with_name("config.yml"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sha256_bytes(data: bytes) -> str:
|
|
97
|
+
return hashlib.sha256(data).hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _file_fingerprint(path_value: Any) -> Dict[str, Any]:
|
|
101
|
+
"""Return a reproducibility fingerprint without forcing huge full-file hashes."""
|
|
102
|
+
if not path_value:
|
|
103
|
+
return {"path": path_value, "exists": False}
|
|
104
|
+
|
|
105
|
+
path = Path(str(path_value))
|
|
106
|
+
record: Dict[str, Any] = {
|
|
107
|
+
"path": str(path),
|
|
108
|
+
"exists": path.exists(),
|
|
109
|
+
}
|
|
110
|
+
if not path.exists() or not path.is_file():
|
|
111
|
+
return record
|
|
112
|
+
|
|
113
|
+
stat = path.stat()
|
|
114
|
+
record.update(
|
|
115
|
+
{
|
|
116
|
+
"size_bytes": stat.st_size,
|
|
117
|
+
"mtime_ns": stat.st_mtime_ns,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
full_hash = (
|
|
122
|
+
os.environ.get("RIBOMETRIC_FULL_INPUT_HASH", "").lower() in {"1", "true", "yes"}
|
|
123
|
+
or stat.st_size <= _FULL_HASH_LIMIT_BYTES
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
h = hashlib.sha256()
|
|
127
|
+
if full_hash:
|
|
128
|
+
with path.open("rb") as handle:
|
|
129
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
130
|
+
h.update(chunk)
|
|
131
|
+
record["sha256"] = h.hexdigest()
|
|
132
|
+
record["hash_method"] = "full_sha256"
|
|
133
|
+
else:
|
|
134
|
+
with path.open("rb") as handle:
|
|
135
|
+
first = handle.read(_HASH_SAMPLE_BYTES)
|
|
136
|
+
if stat.st_size > _HASH_SAMPLE_BYTES:
|
|
137
|
+
handle.seek(max(0, stat.st_size - _HASH_SAMPLE_BYTES))
|
|
138
|
+
last = handle.read(_HASH_SAMPLE_BYTES)
|
|
139
|
+
else:
|
|
140
|
+
last = b""
|
|
141
|
+
h.update(first)
|
|
142
|
+
h.update(last)
|
|
143
|
+
record["sha256_sampled"] = h.hexdigest()
|
|
144
|
+
record["hash_method"] = f"first_last_{_HASH_SAMPLE_BYTES}_bytes_sha256"
|
|
145
|
+
return record
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_run_provenance(
|
|
149
|
+
args: argparse.Namespace,
|
|
150
|
+
config: Dict[str, Any],
|
|
151
|
+
subsampling: Optional[Dict[str, Any]] = None,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
arg_cfg = config.get("argument", {})
|
|
154
|
+
input_keys = [
|
|
155
|
+
"bam",
|
|
156
|
+
"annotation",
|
|
157
|
+
"gff",
|
|
158
|
+
"fasta",
|
|
159
|
+
"json_in",
|
|
160
|
+
"offset_read_length",
|
|
161
|
+
"offset_read_specific",
|
|
162
|
+
]
|
|
163
|
+
config_path = _config_path_used(args)
|
|
164
|
+
provenance = {
|
|
165
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
166
|
+
"command": getattr(args, "command", None),
|
|
167
|
+
"package_version": _package_version(),
|
|
168
|
+
"python": platform.python_version(),
|
|
169
|
+
"platform": platform.platform(),
|
|
170
|
+
"config_file": _file_fingerprint(config_path),
|
|
171
|
+
"effective_config_sha256": _sha256_bytes(
|
|
172
|
+
json.dumps(config, sort_keys=True, default=str).encode("utf-8")
|
|
173
|
+
),
|
|
174
|
+
"inputs": {
|
|
175
|
+
key: _file_fingerprint(arg_cfg.get(key))
|
|
176
|
+
for key in input_keys
|
|
177
|
+
if arg_cfg.get(key) is not None
|
|
178
|
+
},
|
|
179
|
+
"subsampling": (
|
|
180
|
+
subsampling
|
|
181
|
+
if subsampling is not None
|
|
182
|
+
else {
|
|
183
|
+
"requested": arg_cfg.get("subsample"),
|
|
184
|
+
"seed": arg_cfg.get("seed", 42) if arg_cfg.get("subsample") is not None else None,
|
|
185
|
+
"fraction": None,
|
|
186
|
+
"realised_count": None,
|
|
187
|
+
}
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
return provenance
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _package_version() -> str:
|
|
194
|
+
try:
|
|
195
|
+
from . import __version__
|
|
196
|
+
|
|
197
|
+
return str(__version__)
|
|
198
|
+
except Exception:
|
|
199
|
+
return "unknown"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def print_logo(console: Console) -> None:
|
|
203
|
+
"""
|
|
204
|
+
print the logo to the console
|
|
205
|
+
"""
|
|
206
|
+
logo = Text(
|
|
207
|
+
"""
|
|
208
|
+
██████╗ ██╗ ██████╗ ██████╗
|
|
209
|
+
██╔══██╗ ██║ ██╔══██╗██╔═══██╗
|
|
210
|
+
██████╔╝ ██║ ██████╔╝██║ ██║
|
|
211
|
+
██╔══██╗ ██║ ██╔══██╗██║ ██║
|
|
212
|
+
██║ ██║ ██║ ██████╔╝╚██████╔╝
|
|
213
|
+
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
|
|
214
|
+
""",
|
|
215
|
+
style="bold blue",
|
|
216
|
+
)
|
|
217
|
+
logo += Text(
|
|
218
|
+
"""
|
|
219
|
+
███╗ ███╗███████╗█████████╗██████╗ ██╗ ██████╗
|
|
220
|
+
████╗ ████║██╔════╝╚══██╔═══╝██╔══██╗ ██║ ██╔════╝
|
|
221
|
+
██╔████╔██║█████╗ ██║ ██████╔╝ ██║ ██║
|
|
222
|
+
██║╚██╔╝██║██╔══╝ ██║ ██╔══██╗ ██║ ██║
|
|
223
|
+
██║ ╚═╝ ██║███████╗ ██║ ██║ ██║ ██║ ╚██████╗
|
|
224
|
+
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
|
|
225
|
+
""",
|
|
226
|
+
style="bold red",
|
|
227
|
+
)
|
|
228
|
+
console.print(logo)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def print_table_run(
|
|
232
|
+
args: argparse.Namespace, config: Dict[str, Any], console: Console, mode: str
|
|
233
|
+
) -> None:
|
|
234
|
+
console = Console()
|
|
235
|
+
|
|
236
|
+
Inputs = Table(show_header=True, header_style="bold magenta")
|
|
237
|
+
Inputs.add_column("Parameters", style="dim", width=20)
|
|
238
|
+
Inputs.add_column("Values")
|
|
239
|
+
if config["argument"]["bam"]:
|
|
240
|
+
Inputs.add_row("Bam File:", config["argument"]["bam"])
|
|
241
|
+
if config["argument"].get("annotation"):
|
|
242
|
+
Inputs.add_row("Annotation File:", config["argument"]["annotation"])
|
|
243
|
+
elif config["argument"].get("gff"):
|
|
244
|
+
Inputs.add_row("GFF File:", config["argument"]["gff"])
|
|
245
|
+
if config["argument"].get("fasta"):
|
|
246
|
+
Inputs.add_row("Transcriptome File:", config["argument"]["fasta"])
|
|
247
|
+
elif config["argument"]["json_in"]:
|
|
248
|
+
Inputs.add_row("JSON File:", config["argument"]["json_in"])
|
|
249
|
+
|
|
250
|
+
Configs = Table(show_header=True, header_style="bold yellow")
|
|
251
|
+
Configs.add_column("Options", style="dim", width=20)
|
|
252
|
+
Configs.add_column("Values")
|
|
253
|
+
Configs.add_row("Mode:", mode)
|
|
254
|
+
subs = config["argument"].get("subsample")
|
|
255
|
+
trans = config["argument"].get("transcripts")
|
|
256
|
+
Configs.add_row("# of reads:", str(subs) if subs is not None else "Full file")
|
|
257
|
+
Configs.add_row("# of transcripts:", str(trans) if trans is not None else "Full file")
|
|
258
|
+
Configs.add_row("# of threads:", str(config["argument"]["threads"]))
|
|
259
|
+
Configs.add_row("Config file:", args.config)
|
|
260
|
+
|
|
261
|
+
Output = Table(show_header=True, header_style="bold blue")
|
|
262
|
+
Output.add_column("Output Options", style="dim", width=20)
|
|
263
|
+
Output.add_column("Values")
|
|
264
|
+
Output.add_row("JSON:", str(config["argument"]["json"]))
|
|
265
|
+
Output.add_row("HTML:", str(config["argument"]["html"]))
|
|
266
|
+
Output.add_row("PDF:", str(config["argument"]["pdf"]))
|
|
267
|
+
Output.add_row("CSV:", str(config["argument"]["csv"]))
|
|
268
|
+
|
|
269
|
+
# Print tables side by side
|
|
270
|
+
console.print(Inputs, Configs, Output, justify=None, style="bold")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def print_table_prepare(
|
|
274
|
+
args: argparse.Namespace, config: Dict[str, Any], console: Console, mode: str
|
|
275
|
+
) -> None:
|
|
276
|
+
console = Console()
|
|
277
|
+
|
|
278
|
+
Inputs = Table(show_header=True, header_style="bold magenta")
|
|
279
|
+
Inputs.add_column("Parameters", style="dim", width=20)
|
|
280
|
+
Inputs.add_column("Values")
|
|
281
|
+
Inputs.add_row("Gff File:", config["argument"]["gff"])
|
|
282
|
+
|
|
283
|
+
Configs = Table(show_header=True, header_style="bold yellow")
|
|
284
|
+
Configs.add_column("Options", style="dim", width=20)
|
|
285
|
+
Configs.add_column("Values")
|
|
286
|
+
Configs.add_row("Mode:", mode)
|
|
287
|
+
trans = config["argument"].get("transcripts")
|
|
288
|
+
Configs.add_row("# of transcripts:", str(trans) if trans is not None else "Full file")
|
|
289
|
+
Configs.add_row("# of threads:", str(config["argument"]["threads"]))
|
|
290
|
+
Configs.add_row("Config file:", args.config)
|
|
291
|
+
|
|
292
|
+
# Print tables side by side
|
|
293
|
+
console.print(Inputs, Configs, justify=None, style="bold")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main(args: argparse.Namespace) -> int:
|
|
297
|
+
"""
|
|
298
|
+
Main function for the RiboMetric command line interface
|
|
299
|
+
|
|
300
|
+
Inputs:
|
|
301
|
+
args: Namespace object containing the parsed arguments
|
|
302
|
+
|
|
303
|
+
Outputs:
|
|
304
|
+
None
|
|
305
|
+
"""
|
|
306
|
+
# Handle evaluate command separately (no logo or config needed)
|
|
307
|
+
if args.command == "evaluate":
|
|
308
|
+
from .evaluate import evaluate as run_evaluate
|
|
309
|
+
|
|
310
|
+
return run_evaluate(args)
|
|
311
|
+
|
|
312
|
+
# Handle view command separately (no logo or config needed)
|
|
313
|
+
if args.command == "view":
|
|
314
|
+
import json
|
|
315
|
+
|
|
316
|
+
from .tui import run_tui
|
|
317
|
+
|
|
318
|
+
# Validate file exists and is JSON
|
|
319
|
+
file_path = Path(args.json_file)
|
|
320
|
+
if not file_path.exists():
|
|
321
|
+
print(f"Error: File not found: {args.json_file}")
|
|
322
|
+
return 1
|
|
323
|
+
|
|
324
|
+
if not file_path.suffix == ".json":
|
|
325
|
+
print("Error: File must be a JSON file")
|
|
326
|
+
return 1
|
|
327
|
+
|
|
328
|
+
# Validate it's a RiboMetric JSON file
|
|
329
|
+
try:
|
|
330
|
+
with open(file_path, "r") as f:
|
|
331
|
+
data = json.load(f)
|
|
332
|
+
if "results" not in data or "config" not in data:
|
|
333
|
+
print("Warning: File may not be a valid RiboMetric JSON file.")
|
|
334
|
+
print("Expected structure: {'results': {...}, 'config': {...}}")
|
|
335
|
+
except json.JSONDecodeError as e:
|
|
336
|
+
print(f"Error: Invalid JSON file: {e}")
|
|
337
|
+
return 1
|
|
338
|
+
except Exception as e:
|
|
339
|
+
print(f"Error reading file: {e}")
|
|
340
|
+
return 1
|
|
341
|
+
|
|
342
|
+
# Launch TUI
|
|
343
|
+
run_tui(str(file_path))
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
console = Console()
|
|
347
|
+
print_logo(console)
|
|
348
|
+
|
|
349
|
+
config = open_config(args)
|
|
350
|
+
export = config["argument"].copy()
|
|
351
|
+
|
|
352
|
+
# Every output path is built from this directory, but nothing wrote to it
|
|
353
|
+
# until the very end of the run, so a missing directory surfaced as a
|
|
354
|
+
# FileNotFoundError after the whole analysis had already been computed.
|
|
355
|
+
# Create it up front instead.
|
|
356
|
+
output_directory = config["argument"].get("output") or ""
|
|
357
|
+
if output_directory:
|
|
358
|
+
try:
|
|
359
|
+
Path(output_directory).mkdir(parents=True, exist_ok=True)
|
|
360
|
+
except OSError as exc:
|
|
361
|
+
raise SystemExit(f"Cannot use output directory '{output_directory}': {exc}")
|
|
362
|
+
|
|
363
|
+
# Handle inputs and run modes appropriately
|
|
364
|
+
if args.command == "prepare":
|
|
365
|
+
print_table_prepare(args, config, console, "Prepare Mode")
|
|
366
|
+
prepare_annotation(
|
|
367
|
+
config["argument"]["gff"],
|
|
368
|
+
config["argument"]["output"],
|
|
369
|
+
config["argument"]["transcripts"],
|
|
370
|
+
config["argument"]["threads"],
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
else:
|
|
374
|
+
print_table_run(args, config, console, "Run Mode")
|
|
375
|
+
|
|
376
|
+
if config["argument"]["bam"]:
|
|
377
|
+
if not check_bam(config["argument"]["bam"]):
|
|
378
|
+
raise Exception("""
|
|
379
|
+
Either BAM file or it's index does not exist at given path
|
|
380
|
+
|
|
381
|
+
To create an index for a BAM file, run:
|
|
382
|
+
samtools index <bam_file>
|
|
383
|
+
""")
|
|
384
|
+
|
|
385
|
+
if config["argument"]["annotation"] is not None:
|
|
386
|
+
if not check_annotation(config["argument"]["annotation"]):
|
|
387
|
+
raise Exception("""
|
|
388
|
+
Annotation file not found or not in the correct format.
|
|
389
|
+
|
|
390
|
+
To create an annotation file, run:
|
|
391
|
+
RiboMetric prepare -g <gff_file>
|
|
392
|
+
""")
|
|
393
|
+
|
|
394
|
+
flagstat = flagstat_bam(config["argument"]["bam"])
|
|
395
|
+
# Parse all primary alignments before selection so -S is not tied
|
|
396
|
+
# to BAM/reference split order.
|
|
397
|
+
read_limit = flagstat["mapped_reads"]
|
|
398
|
+
|
|
399
|
+
# Parse the bam file
|
|
400
|
+
read_df_pre, sequence_data, sequence_background = parse_bam(
|
|
401
|
+
bam_file=config["argument"]["bam"],
|
|
402
|
+
num_reads=read_limit,
|
|
403
|
+
num_processes=config["argument"]["threads"],
|
|
404
|
+
)
|
|
405
|
+
subsampling = None
|
|
406
|
+
# Direct legacy callers may construct a Namespace without the new
|
|
407
|
+
# seed field; retain their fixture semantics. CLI parses always
|
|
408
|
+
# carry the default seed and therefore take the reproducible path.
|
|
409
|
+
if config["argument"].get("subsample") is not None and hasattr(args, "seed"):
|
|
410
|
+
subsampling_seed = int(config["argument"].get("seed", 42))
|
|
411
|
+
read_df_pre, subsampling = deterministic_subsample(
|
|
412
|
+
read_df_pre, config["argument"]["subsample"], subsampling_seed
|
|
413
|
+
)
|
|
414
|
+
sequence_data, sequence_background = recompute_sequence_summaries(read_df_pre)
|
|
415
|
+
if read_df_pre.empty:
|
|
416
|
+
raise Exception("""
|
|
417
|
+
No reads found in the given bam file.
|
|
418
|
+
|
|
419
|
+
Please check the file and try again.
|
|
420
|
+
""")
|
|
421
|
+
print("Reads parsed")
|
|
422
|
+
|
|
423
|
+
# Optionally drop sequence data so sequence-based metrics are
|
|
424
|
+
# skipped (explicit flag, or BAMs with no stored sequences). #122
|
|
425
|
+
if config["argument"].get("skip_sequence_metrics"):
|
|
426
|
+
print("Skipping sequence-based metrics (--skip-sequence-metrics)")
|
|
427
|
+
sequence_data, sequence_background = {}, {}
|
|
428
|
+
elif not sequence_background:
|
|
429
|
+
print("No stored sequences detected; " "sequence-based metrics will be skipped.")
|
|
430
|
+
|
|
431
|
+
# Use weighted computations downstream instead of expanding rows
|
|
432
|
+
if "count" not in read_df_pre.columns:
|
|
433
|
+
read_df_pre["count"] = 1
|
|
434
|
+
read_df = read_df_pre
|
|
435
|
+
del read_df_pre
|
|
436
|
+
|
|
437
|
+
# Parse FASTA up-front so it can be passed into annotation_mode
|
|
438
|
+
# for RUST and other sequence-level metrics.
|
|
439
|
+
fasta_dict = None
|
|
440
|
+
if config["argument"]["fasta"] is not None:
|
|
441
|
+
print("Parsing FASTA for sequence-level metrics...")
|
|
442
|
+
fasta_dict = parse_fasta(config["argument"]["fasta"])
|
|
443
|
+
|
|
444
|
+
if config["argument"]["gff"] is None and config["argument"]["annotation"] is None:
|
|
445
|
+
results_dict = annotation_mode(
|
|
446
|
+
read_df,
|
|
447
|
+
sequence_data,
|
|
448
|
+
sequence_background,
|
|
449
|
+
config=config,
|
|
450
|
+
fasta_dict=fasta_dict,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
else:
|
|
454
|
+
if (
|
|
455
|
+
config["argument"]["annotation"] is not None
|
|
456
|
+
and config["argument"]["gff"] is not None
|
|
457
|
+
):
|
|
458
|
+
print("Running annotation mode")
|
|
459
|
+
annotation_df = parse_annotation(config["argument"]["annotation"])
|
|
460
|
+
# Ensure annotation mode actually runs for this branch
|
|
461
|
+
results_dict = annotation_mode(
|
|
462
|
+
read_df,
|
|
463
|
+
sequence_data,
|
|
464
|
+
sequence_background,
|
|
465
|
+
annotation_df,
|
|
466
|
+
config,
|
|
467
|
+
fasta_dict=fasta_dict,
|
|
468
|
+
)
|
|
469
|
+
elif (
|
|
470
|
+
config["argument"]["annotation"] is None
|
|
471
|
+
and config["argument"]["gff"] is not None
|
|
472
|
+
):
|
|
473
|
+
print("Gff provided, preparing annotation")
|
|
474
|
+
annotation_df = prepare_annotation(
|
|
475
|
+
config["argument"]["gff"],
|
|
476
|
+
config["argument"]["output"],
|
|
477
|
+
config["argument"]["transcripts"],
|
|
478
|
+
config["argument"]["threads"],
|
|
479
|
+
)
|
|
480
|
+
print("Annotation prepared")
|
|
481
|
+
# Run annotation mode after preparing annotation
|
|
482
|
+
results_dict = annotation_mode(
|
|
483
|
+
read_df,
|
|
484
|
+
sequence_data,
|
|
485
|
+
sequence_background,
|
|
486
|
+
annotation_df,
|
|
487
|
+
config,
|
|
488
|
+
fasta_dict=fasta_dict,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
elif (
|
|
492
|
+
config["argument"]["annotation"] is not None
|
|
493
|
+
and config["argument"]["gff"] is None
|
|
494
|
+
):
|
|
495
|
+
print("Annotation provided, parsing")
|
|
496
|
+
annotation_df = parse_annotation(config["argument"]["annotation"])
|
|
497
|
+
print("Annotation parsed")
|
|
498
|
+
|
|
499
|
+
print("Running annotation mode")
|
|
500
|
+
results_dict = annotation_mode(
|
|
501
|
+
read_df,
|
|
502
|
+
sequence_data,
|
|
503
|
+
sequence_background,
|
|
504
|
+
annotation_df,
|
|
505
|
+
config,
|
|
506
|
+
fasta_dict=fasta_dict,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Merge samtools flagstat data into alignment_stats so the report
|
|
510
|
+
# can display total_reads, mapping_rate, etc.
|
|
511
|
+
results_dict.setdefault("alignment_stats", {}).update(flagstat)
|
|
512
|
+
results_dict["provenance"] = _build_run_provenance(args, config, subsampling)
|
|
513
|
+
|
|
514
|
+
filename = config["argument"]["bam"].split("/")[-1]
|
|
515
|
+
if "." in filename:
|
|
516
|
+
filename = filename.split(".")[:-1]
|
|
517
|
+
|
|
518
|
+
elif config["argument"]["json_in"]:
|
|
519
|
+
print("JSON input provided")
|
|
520
|
+
filename = config["argument"]["json_in"].split("/")[-1]
|
|
521
|
+
if "." in filename:
|
|
522
|
+
filename = filename.split(".")[:-1]
|
|
523
|
+
|
|
524
|
+
json_dicts = parse_json_input(config["argument"]["json_in"])
|
|
525
|
+
results_dict = json_dicts[0]
|
|
526
|
+
json_config = json_dicts[1]
|
|
527
|
+
config["argument"] = json_config["argument"]
|
|
528
|
+
|
|
529
|
+
if config["argument"]["json_config"]:
|
|
530
|
+
config["plots"] = json_config["plots"]
|
|
531
|
+
results_dict["provenance"] = _build_run_provenance(args, config)
|
|
532
|
+
|
|
533
|
+
# Indentify output requirements
|
|
534
|
+
if export["name"] is not None:
|
|
535
|
+
filename = export["name"]
|
|
536
|
+
|
|
537
|
+
report_prefix = f"{''.join(filename)}_RiboMetric"
|
|
538
|
+
|
|
539
|
+
if export.get("html"):
|
|
540
|
+
if export["pdf"]:
|
|
541
|
+
report_export = "both"
|
|
542
|
+
else:
|
|
543
|
+
report_export = "html"
|
|
544
|
+
elif export.get("pdf"):
|
|
545
|
+
report_export = "pdf"
|
|
546
|
+
else:
|
|
547
|
+
report_export = None
|
|
548
|
+
# Write out the specified output files
|
|
549
|
+
if report_export is not None:
|
|
550
|
+
plots_list = generate_plots(results_dict, config)
|
|
551
|
+
generate_report(plots_list, config, report_export, report_prefix, export["output"])
|
|
552
|
+
|
|
553
|
+
if export.get("json"):
|
|
554
|
+
generate_json(results_dict, config, report_prefix, export["output"])
|
|
555
|
+
|
|
556
|
+
if export.get("csv"):
|
|
557
|
+
generate_csv(results_dict, config, report_prefix, export["output"])
|
|
558
|
+
|
|
559
|
+
# Improved outputs (low-hanging fruit)
|
|
560
|
+
sample_name = "".join(filename)
|
|
561
|
+
if export.get("improved_outputs"):
|
|
562
|
+
generate_all_outputs(
|
|
563
|
+
results_dict,
|
|
564
|
+
config,
|
|
565
|
+
sample_name,
|
|
566
|
+
export.get("output", ""),
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
if export.get("summary_tsv"):
|
|
570
|
+
generate_summary_tsv(
|
|
571
|
+
results_dict,
|
|
572
|
+
config,
|
|
573
|
+
sample_name,
|
|
574
|
+
f"{sample_name}_summary.tsv",
|
|
575
|
+
export.get("output", ""),
|
|
576
|
+
)
|
|
577
|
+
if export.get("metrics_table"):
|
|
578
|
+
generate_metrics_table_csv(
|
|
579
|
+
results_dict,
|
|
580
|
+
config,
|
|
581
|
+
sample_name,
|
|
582
|
+
f"{sample_name}_metrics_table.csv",
|
|
583
|
+
export.get("output", ""),
|
|
584
|
+
)
|
|
585
|
+
if export.get("qc_status"):
|
|
586
|
+
generate_qc_status(
|
|
587
|
+
results_dict,
|
|
588
|
+
config,
|
|
589
|
+
sample_name,
|
|
590
|
+
None,
|
|
591
|
+
f"{sample_name}_qc_status.json",
|
|
592
|
+
export.get("output", ""),
|
|
593
|
+
)
|
|
594
|
+
if export.get("comparison_csv"):
|
|
595
|
+
generate_comparison_ready_csv(
|
|
596
|
+
results_dict,
|
|
597
|
+
config,
|
|
598
|
+
sample_name,
|
|
599
|
+
f"{sample_name}_comparison.csv",
|
|
600
|
+
export.get("output", ""),
|
|
601
|
+
)
|
|
602
|
+
if export.get("offsets_tsv"):
|
|
603
|
+
generate_offsets_tsv(
|
|
604
|
+
results_dict,
|
|
605
|
+
sample_name,
|
|
606
|
+
f"{sample_name}_offsets.tsv",
|
|
607
|
+
export.get("output", ""),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if export.get("output_offsets"):
|
|
611
|
+
output_path = Path(export["output_offsets"])
|
|
612
|
+
generate_offsets_tsv(
|
|
613
|
+
results_dict,
|
|
614
|
+
sample_name,
|
|
615
|
+
output_path.name,
|
|
616
|
+
str(output_path.parent) if str(output_path.parent) != "." else "",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
return 0
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
if __name__ == "__main__":
|
|
623
|
+
parser = argument_parser()
|
|
624
|
+
args = parser.parse_args()
|
|
625
|
+
|
|
626
|
+
# -b/--bam vs -j/--json-in exclusivity is enforced by argparse's
|
|
627
|
+
# mutually-exclusive group on the `run` subparser; no manual check needed
|
|
628
|
+
# here (the previous one assumed those attributes existed for every
|
|
629
|
+
# subcommand and crashed for prepare/evaluate/view).
|
|
630
|
+
if getattr(args, "command", None) is None:
|
|
631
|
+
parser.print_help()
|
|
632
|
+
raise SystemExit(0)
|
|
633
|
+
|
|
634
|
+
raise SystemExit(main(args) or 0)
|