alnkit 0.2.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.
- alnkit/__init__.py +11 -0
- alnkit/__main__.py +10 -0
- alnkit/_docs.py +53 -0
- alnkit/cli/__init__.py +53 -0
- alnkit/cli/__main__.py +10 -0
- alnkit/cli/_common.py +25 -0
- alnkit/cli/align.py +52 -0
- alnkit/cli/doctor.py +81 -0
- alnkit/cli/long.py +346 -0
- alnkit/cli/plot.py +293 -0
- alnkit/cli/ref.py +50 -0
- alnkit/cli/seg.py +131 -0
- alnkit/cli/selftest.py +308 -0
- alnkit/cli/short.py +245 -0
- alnkit/core/__init__.py +9 -0
- alnkit/core/bam_segments.py +183 -0
- alnkit/core/coords.py +129 -0
- alnkit/core/errors.py +25 -0
- alnkit/core/external.py +63 -0
- alnkit/core/fasta.py +83 -0
- alnkit/core/logging.py +25 -0
- alnkit/core/paf.py +52 -0
- alnkit/core/segments.py +253 -0
- alnkit/core/tables.py +340 -0
- alnkit/longread/__init__.py +5 -0
- alnkit/longread/assemble.py +453 -0
- alnkit/longread/breakpoints.py +910 -0
- alnkit/longread/classify.py +958 -0
- alnkit/longread/graph.py +174 -0
- alnkit/longread/locate.py +904 -0
- alnkit/plot/__init__.py +5 -0
- alnkit/plot/ribbon.py +1306 -0
- alnkit/plot/tracks.py +1035 -0
- alnkit/prepare.py +256 -0
- alnkit/py.typed +0 -0
- alnkit/shortread/__init__.py +20 -0
- alnkit/shortread/bam.py +527 -0
- alnkit/shortread/evidence.py +214 -0
- alnkit/shortread/interference.py +44 -0
- alnkit/shortread/merge.py +126 -0
- alnkit/shortread/model.py +301 -0
- alnkit/shortread/origin.py +507 -0
- alnkit/shortread/report.py +316 -0
- alnkit/shortread/scan.py +788 -0
- alnkit-0.2.0.dist-info/METADATA +163 -0
- alnkit-0.2.0.dist-info/RECORD +49 -0
- alnkit-0.2.0.dist-info/WHEEL +4 -0
- alnkit-0.2.0.dist-info/entry_points.txt +2 -0
- alnkit-0.2.0.dist-info/licenses/LICENSE +674 -0
alnkit/cli/selftest.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""``alnkit selftest``:不依赖任何输入数据的断言集。
|
|
2
|
+
|
|
3
|
+
M0 覆盖坐标换算与表/段契约;二代 B1 的 30+ 条口径断言在 P2 并入这里
|
|
4
|
+
。任何一条失败 → 退出码 1。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..core import coords, segments, tables
|
|
14
|
+
from ..core.errors import UsageError
|
|
15
|
+
from ._common import GLOBAL_OPTIONS
|
|
16
|
+
|
|
17
|
+
_CHECKS: list[tuple[str, Callable[[], None]]] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
|
|
21
|
+
"""把一个无参函数注册成一条自检(失败即抛异常)。"""
|
|
22
|
+
|
|
23
|
+
def register(func: Callable[[], None]) -> Callable[[], None]:
|
|
24
|
+
_CHECKS.append((name, func))
|
|
25
|
+
return func
|
|
26
|
+
|
|
27
|
+
return register
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
31
|
+
parser = subparsers.add_parser(
|
|
32
|
+
"selftest",
|
|
33
|
+
parents=[GLOBAL_OPTIONS],
|
|
34
|
+
help="运行内置自检(不需要输入数据)",
|
|
35
|
+
description="校验坐标换算与表契约;任何一条失败则退出码为 1。",
|
|
36
|
+
epilog="例子:\n alnkit selftest",
|
|
37
|
+
)
|
|
38
|
+
parser.set_defaults(handler=run)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------ coords
|
|
42
|
+
@check("coords: 1-based <-> 0-based 往返")
|
|
43
|
+
def _coords_position_roundtrip() -> None:
|
|
44
|
+
for position_0based in (0, 1, 9424, 1_000_000, 10**9):
|
|
45
|
+
back = coords.to_0based(coords.to_1based(position_0based))
|
|
46
|
+
assert back == position_0based, (position_0based, back)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@check("coords: 区间串往返(1-based 闭 <-> 0-based 半开)")
|
|
50
|
+
def _coords_region_roundtrip() -> None:
|
|
51
|
+
text = coords.format_region_1based("chr1", 2_000_000, 2_000_050)
|
|
52
|
+
assert text == "chr1:2000001-2000050", text
|
|
53
|
+
assert coords.parse_region_1based(text) == ("chr1", 2_000_000, 2_000_050)
|
|
54
|
+
assert coords.parse_region_1based("chr1:1,000-2,000") == ("chr1", 999, 2000)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@check("coords: 区间换算互逆")
|
|
58
|
+
def _coords_interval_roundtrip() -> None:
|
|
59
|
+
for start_0based, end_0based in ((0, 1), (99, 200), (2_000_000, 2_000_050)):
|
|
60
|
+
start_1based, end_1based = coords.interval_0based_to_1based(start_0based, end_0based)
|
|
61
|
+
assert (start_1based, end_1based) == (start_0based + 1, end_0based)
|
|
62
|
+
assert coords.interval_1based_to_0based(start_1based, end_1based) == (
|
|
63
|
+
start_0based,
|
|
64
|
+
end_0based,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@check("coords: 断点边界 / 左碱基恒等式(Insert_Position 口径)")
|
|
69
|
+
def _coords_boundary_identity() -> None:
|
|
70
|
+
boundary = 1_000_001
|
|
71
|
+
assert coords.left_base_1based(boundary) == 1_000_001
|
|
72
|
+
assert coords.left_base_0based(boundary) == 1_000_000
|
|
73
|
+
assert coords.left_base_0based(boundary) + 1 == coords.left_base_1based(boundary)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@check("coords: 反向链 read 区间翻转")
|
|
77
|
+
def _coords_flip_read_interval() -> None:
|
|
78
|
+
assert coords.flip_read_interval(40_592, 9_424, 40_591) == (1, 31_168)
|
|
79
|
+
assert coords.flip_read_interval(150, 0, 150) == (0, 150)
|
|
80
|
+
try:
|
|
81
|
+
coords.flip_read_interval(100, 10, 200)
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
else: # pragma: no cover - 防御式断言
|
|
85
|
+
raise AssertionError("越界区间应当报错")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------ tables
|
|
89
|
+
@check("tables: 所有注册表满足坐标后缀规则")
|
|
90
|
+
def _tables_registry_valid() -> None:
|
|
91
|
+
for table in tables.COLUMNS:
|
|
92
|
+
tables.validate_columns(table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@check("tables: 坐标列缺少基准后缀会被拒绝")
|
|
96
|
+
def _tables_reject_bare_coordinate() -> None:
|
|
97
|
+
try:
|
|
98
|
+
tables.validate_column_names("demo", ("ref_start", "read_end"))
|
|
99
|
+
except AssertionError:
|
|
100
|
+
return
|
|
101
|
+
raise AssertionError("裸坐标列名 ref_start/read_end 应当被拒绝")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@check("tables: 写入的列序与注册表一致")
|
|
105
|
+
def _tables_column_order() -> None:
|
|
106
|
+
columns = tables.columns_of("insert_sites")
|
|
107
|
+
assert columns[0] == "chrom"
|
|
108
|
+
assert columns[1] == "insert_pos_1based"
|
|
109
|
+
assert columns[3] == "insert_boundary_0based"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------- segments
|
|
113
|
+
_SAME_NUMBERS = {
|
|
114
|
+
"read_name": "r1",
|
|
115
|
+
"chrom": "chr1",
|
|
116
|
+
"ref_start": 9_000_000,
|
|
117
|
+
"ref_end": 9_031_165,
|
|
118
|
+
"read_start": 9_424,
|
|
119
|
+
"read_end": 40_591,
|
|
120
|
+
"strand": "-",
|
|
121
|
+
"mapq": 60,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@check("segments: 三套历史表头解析出同一组坐标")
|
|
126
|
+
def _segments_three_legacy_headers() -> None:
|
|
127
|
+
alignment_table_header = [
|
|
128
|
+
"chrom",
|
|
129
|
+
"chrom_length",
|
|
130
|
+
"ref_pos_start",
|
|
131
|
+
"ref_pos_end",
|
|
132
|
+
"read_name",
|
|
133
|
+
"read_pos_start",
|
|
134
|
+
"read_pos_end",
|
|
135
|
+
"strand",
|
|
136
|
+
"mapping_quality",
|
|
137
|
+
"is_secondary",
|
|
138
|
+
"is_supplementary",
|
|
139
|
+
"ref_aln_len",
|
|
140
|
+
"read_aln_len",
|
|
141
|
+
"simplified_cigar",
|
|
142
|
+
]
|
|
143
|
+
alignment_table_row = {
|
|
144
|
+
"chrom": "chr1",
|
|
145
|
+
"chrom_length": "100000000",
|
|
146
|
+
"ref_pos_start": "9000000",
|
|
147
|
+
"ref_pos_end": "9031165",
|
|
148
|
+
"read_name": "r1",
|
|
149
|
+
"read_pos_start": "9424",
|
|
150
|
+
"read_pos_end": "40591",
|
|
151
|
+
"strand": "-",
|
|
152
|
+
"mapping_quality": "60",
|
|
153
|
+
"is_secondary": "0",
|
|
154
|
+
"is_supplementary": "0",
|
|
155
|
+
"ref_aln_len": "31165",
|
|
156
|
+
"read_aln_len": "31167",
|
|
157
|
+
"simplified_cigar": "1S31204M9424S",
|
|
158
|
+
}
|
|
159
|
+
classify_header = [
|
|
160
|
+
"read_name",
|
|
161
|
+
"segment_role",
|
|
162
|
+
"chrom",
|
|
163
|
+
"chrom_length",
|
|
164
|
+
"ref_start",
|
|
165
|
+
"ref_end",
|
|
166
|
+
"read_start",
|
|
167
|
+
"read_end",
|
|
168
|
+
"strand",
|
|
169
|
+
"mapq",
|
|
170
|
+
"ref_aln_len",
|
|
171
|
+
"read_aln_len",
|
|
172
|
+
"simplified_cigar",
|
|
173
|
+
"kept",
|
|
174
|
+
"drop_reason",
|
|
175
|
+
]
|
|
176
|
+
classify_row = {
|
|
177
|
+
"read_name": "r1",
|
|
178
|
+
"segment_role": "genome",
|
|
179
|
+
"chrom": "chr1",
|
|
180
|
+
"chrom_length": "100000000",
|
|
181
|
+
"ref_start": "9000000",
|
|
182
|
+
"ref_end": "9031165",
|
|
183
|
+
"read_start": "9424",
|
|
184
|
+
"read_end": "40591",
|
|
185
|
+
"strand": "-",
|
|
186
|
+
"mapq": "60",
|
|
187
|
+
"ref_aln_len": "31165",
|
|
188
|
+
"read_aln_len": "31167",
|
|
189
|
+
"simplified_cigar": "1S31204M9424S",
|
|
190
|
+
"kept": "1",
|
|
191
|
+
"drop_reason": "-",
|
|
192
|
+
}
|
|
193
|
+
tracks_header = [
|
|
194
|
+
"read_name",
|
|
195
|
+
"read_length",
|
|
196
|
+
"segment_type",
|
|
197
|
+
"target_name",
|
|
198
|
+
"target_start",
|
|
199
|
+
"target_end",
|
|
200
|
+
"read_start",
|
|
201
|
+
"read_end",
|
|
202
|
+
"strand",
|
|
203
|
+
"mapq",
|
|
204
|
+
]
|
|
205
|
+
tracks_row = {
|
|
206
|
+
"read_name": "r1",
|
|
207
|
+
"read_length": "40592",
|
|
208
|
+
"segment_type": "genome",
|
|
209
|
+
"target_name": "chr1",
|
|
210
|
+
"target_start": "9000000",
|
|
211
|
+
"target_end": "9031165",
|
|
212
|
+
"read_start": "9424",
|
|
213
|
+
"read_end": "40591",
|
|
214
|
+
"strand": "-",
|
|
215
|
+
"mapq": "60",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
parsed = [
|
|
219
|
+
segments.segments_from_rows(header, [row])[0]
|
|
220
|
+
for header, row in (
|
|
221
|
+
(alignment_table_header, alignment_table_row),
|
|
222
|
+
(classify_header, classify_row),
|
|
223
|
+
(tracks_header, tracks_row),
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
expected = (
|
|
227
|
+
_SAME_NUMBERS["ref_start"],
|
|
228
|
+
_SAME_NUMBERS["ref_end"],
|
|
229
|
+
_SAME_NUMBERS["read_start"],
|
|
230
|
+
_SAME_NUMBERS["read_end"],
|
|
231
|
+
)
|
|
232
|
+
for segment in parsed:
|
|
233
|
+
got = (
|
|
234
|
+
segment.ref_start_0based,
|
|
235
|
+
segment.ref_end_0based,
|
|
236
|
+
segment.read_start_0based,
|
|
237
|
+
segment.read_end_0based,
|
|
238
|
+
)
|
|
239
|
+
assert got == expected, (got, expected)
|
|
240
|
+
assert segment.chrom == "chr1"
|
|
241
|
+
assert segment.strand == "-"
|
|
242
|
+
assert segment.mapq == 60
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@check("segments: 缺列 / 倒置区间 / 坏 strand 都报 UsageError")
|
|
246
|
+
def _segments_error_paths() -> None:
|
|
247
|
+
good_header = [
|
|
248
|
+
"read_name",
|
|
249
|
+
"chrom",
|
|
250
|
+
"ref_start",
|
|
251
|
+
"ref_end",
|
|
252
|
+
"read_start",
|
|
253
|
+
"read_end",
|
|
254
|
+
"strand",
|
|
255
|
+
"mapq",
|
|
256
|
+
]
|
|
257
|
+
good_row = {
|
|
258
|
+
"read_name": "r1",
|
|
259
|
+
"chrom": "chr1",
|
|
260
|
+
"ref_start": "1",
|
|
261
|
+
"ref_end": "2",
|
|
262
|
+
"read_start": "0",
|
|
263
|
+
"read_end": "1",
|
|
264
|
+
"strand": "+",
|
|
265
|
+
"mapq": "60",
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
cases: list[tuple[list[str], dict[str, Any]]] = [
|
|
269
|
+
(good_header, {**good_row, "ref_start": "10", "ref_end": "5"}),
|
|
270
|
+
(good_header, {**good_row, "read_start": "9", "read_end": "1"}),
|
|
271
|
+
(good_header, {**good_row, "strand": "?"}),
|
|
272
|
+
(["read_name", "chrom"], {"read_name": "r1", "chrom": "chr1"}),
|
|
273
|
+
]
|
|
274
|
+
for header, row in cases:
|
|
275
|
+
try:
|
|
276
|
+
segments.segments_from_rows(header, [row])
|
|
277
|
+
except UsageError:
|
|
278
|
+
continue
|
|
279
|
+
raise AssertionError(f"应当报 UsageError:{header} {row}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@check("short scan: 断点坐标 / 归属 / 窗口合并口径(原 parse_bam --self-test)")
|
|
283
|
+
def _short_scan_self_test() -> None:
|
|
284
|
+
import contextlib
|
|
285
|
+
import io
|
|
286
|
+
|
|
287
|
+
from ..shortread import scan
|
|
288
|
+
|
|
289
|
+
buffer = io.StringIO()
|
|
290
|
+
with contextlib.redirect_stdout(buffer):
|
|
291
|
+
scan.run_self_test()
|
|
292
|
+
assert "self-test 通过" in buffer.getvalue()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def run(args: argparse.Namespace) -> int:
|
|
296
|
+
del args
|
|
297
|
+
failures: list[str] = []
|
|
298
|
+
total = len(_CHECKS)
|
|
299
|
+
for name, func in _CHECKS:
|
|
300
|
+
try:
|
|
301
|
+
func()
|
|
302
|
+
except Exception as exc: # noqa: BLE001 - 自检要汇总所有失败
|
|
303
|
+
failures.append(name)
|
|
304
|
+
print(f"FAIL {name}\n {type(exc).__name__}: {exc}")
|
|
305
|
+
else:
|
|
306
|
+
print(f"PASS {name}")
|
|
307
|
+
print(f"\n{total - len(failures)}/{total} 通过")
|
|
308
|
+
return 1 if failures else 0
|
alnkit/cli/short.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""短读长子命令:``alnkit short scan``(B1)。
|
|
2
|
+
|
|
3
|
+
参数名、默认值、帮助文本与旧脚本 ``parse_bam.py`` 一一对应;只把 ``--version`` 与
|
|
4
|
+
``--self-test`` 换成了 alnkit 的全局选项。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..shortread import scan
|
|
13
|
+
from ._common import GLOBAL_OPTIONS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
17
|
+
short = subparsers.add_parser(
|
|
18
|
+
"short",
|
|
19
|
+
help="短读长链路(二代插入断点证据)",
|
|
20
|
+
description="从 BAM 找外源序列插入参考基因组的断点证据(SR / PESR / DUP)。",
|
|
21
|
+
)
|
|
22
|
+
short_subparsers = short.add_subparsers(dest="short_command", metavar="<subcommand>")
|
|
23
|
+
short_subparsers.required = True
|
|
24
|
+
_add_scan(short_subparsers)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _add_scan(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
28
|
+
parser = subparsers.add_parser(
|
|
29
|
+
"scan",
|
|
30
|
+
parents=[GLOBAL_OPTIONS],
|
|
31
|
+
help="BAM -> 断点证据三张表",
|
|
32
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
33
|
+
description=(
|
|
34
|
+
"一条跨越“参考基因组 <-> 外源插入序列”的嵌合比对可定位断点;"
|
|
35
|
+
"本命令按断点聚合证据(SR / PESR / DUP)。"
|
|
36
|
+
),
|
|
37
|
+
epilog=(
|
|
38
|
+
"上游 BAM 的比对要求(详见原脚本 README):\n"
|
|
39
|
+
" 必须 -Y(补充比对用 soft clip,只从 CIGAR 的 S 取断点);\n"
|
|
40
|
+
" 禁止 -M(会把补充比对标成 secondary,证据变少);\n"
|
|
41
|
+
" 建议 -a -h 20;比对后不要用 samtools -F 2048 / -F 256 / -q 过滤。\n"
|
|
42
|
+
"\n示例:\n"
|
|
43
|
+
" alnkit short scan --bam-dir ./bam --query-chr target_ref \\\n"
|
|
44
|
+
" --insert-chrs decoy_ref,homolog_ref,target_ref \\\n"
|
|
45
|
+
" --merge-window-bp 10 --out-dir out\n"
|
|
46
|
+
" alnkit short scan --self-test"
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--bam",
|
|
51
|
+
action="append",
|
|
52
|
+
type=Path,
|
|
53
|
+
default=[],
|
|
54
|
+
metavar="PATH",
|
|
55
|
+
help="直接指定 BAM(可重复)。与 --bam-dir 二选一",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--bam-dir",
|
|
59
|
+
action="store",
|
|
60
|
+
type=Path,
|
|
61
|
+
default=Path.cwd(),
|
|
62
|
+
metavar="DIR",
|
|
63
|
+
help="扫描 BAM 的目录,默认当前目录",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--bam-glob",
|
|
67
|
+
action="store",
|
|
68
|
+
default="*.sorted.bam",
|
|
69
|
+
metavar="PATTERN",
|
|
70
|
+
help="在 --bam-dir 下匹配 BAM 的通配符,默认 *.sorted.bam",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument("--no-recursive", action="store_true", help="不递归子目录(默认递归)")
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--out-dir",
|
|
75
|
+
action="store",
|
|
76
|
+
type=Path,
|
|
77
|
+
default=Path.cwd(),
|
|
78
|
+
metavar="DIR",
|
|
79
|
+
help="结果输出目录,默认当前目录",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--sample-name",
|
|
83
|
+
action="store",
|
|
84
|
+
default=None,
|
|
85
|
+
metavar="NAME",
|
|
86
|
+
help="样本名(输出文件名前缀),默认取 BAM 文件名首个 . 之前的部分",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--query-chr",
|
|
90
|
+
action="store",
|
|
91
|
+
default=None,
|
|
92
|
+
metavar="NAME",
|
|
93
|
+
help="本次要出结果的外源插入序列名(必填,除非 --self-test)",
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--insert-chrs",
|
|
97
|
+
action="store",
|
|
98
|
+
default=None,
|
|
99
|
+
metavar="NAME1,NAME2,...",
|
|
100
|
+
help="参考序列里全部外源/同源序列名,逗号分隔;默认只含 --query-chr",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--insert-origin-policy",
|
|
104
|
+
action="store",
|
|
105
|
+
choices=("owned", "owned-strict", "lenient", "consensus", "legacy"),
|
|
106
|
+
default="owned",
|
|
107
|
+
help="共有 read 的区分策略:owned(默认)/owned-strict/lenient/consensus/legacy",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--merge-window-bp",
|
|
111
|
+
action="store",
|
|
112
|
+
type=int,
|
|
113
|
+
default=0,
|
|
114
|
+
metavar="INT",
|
|
115
|
+
help="位点窗口合并:相距 <= 该值的断点合并为一个位点,0=不合并(默认);推荐 10",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--min-mapq",
|
|
119
|
+
action="store",
|
|
120
|
+
type=int,
|
|
121
|
+
default=20,
|
|
122
|
+
metavar="INT",
|
|
123
|
+
help="比对质量 <= 该值则丢弃,默认 20",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--max-mismatch",
|
|
127
|
+
action="store",
|
|
128
|
+
type=int,
|
|
129
|
+
default=3,
|
|
130
|
+
metavar="INT",
|
|
131
|
+
help="错配数 > 该值则丢弃,默认 3",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--max-softclip-blocks",
|
|
135
|
+
action="store",
|
|
136
|
+
type=int,
|
|
137
|
+
default=1,
|
|
138
|
+
metavar="INT",
|
|
139
|
+
help="soft clip 段数 > 该值则丢弃,默认 1",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--min-clip-length",
|
|
143
|
+
action="store",
|
|
144
|
+
type=int,
|
|
145
|
+
default=0,
|
|
146
|
+
metavar="INT",
|
|
147
|
+
help="作为断点证据时要求的最小 soft clip 长度,默认 0(不限制)",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--keep-secondary", action="store_true", help="保留 secondary 比对(默认丢弃)"
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--mismatch-source",
|
|
154
|
+
action="store",
|
|
155
|
+
choices=("nm", "md"),
|
|
156
|
+
default="nm",
|
|
157
|
+
help="错配数来源:nm=NM 标签(默认);md=由 MD 推导(历史行为)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--on-missing-tag",
|
|
161
|
+
action="store",
|
|
162
|
+
choices=("skip", "error"),
|
|
163
|
+
default="skip",
|
|
164
|
+
help="缺少 NM/MD 标签时:skip=丢弃该比对(默认);error=报错",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--no-split-match",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="关闭「目标序列上要求存在等长 split 比对」的校验(默认开启)",
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--no-candidate-gate",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help="关闭候选 read 闸门(等价 2.4.0 全量逐条处理);legacy 策略下自动关闭",
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--no-duplicate-filter",
|
|
178
|
+
action="store_true",
|
|
179
|
+
help="保留被 markdup 标记为重复的 read 作为证据(默认剔除,只写 dup 列)",
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--require-dup-flags",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="BAM 里没有任何 FLAG 0x400(未做 markdup)时报错退出(退出码 2)",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--bam-threads",
|
|
188
|
+
action="store",
|
|
189
|
+
type=int,
|
|
190
|
+
default=1,
|
|
191
|
+
metavar="INT",
|
|
192
|
+
help="BAM 解压线程数,默认 1;大文件可调到 2-4",
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"--legacy-coordinates",
|
|
196
|
+
action="store_true",
|
|
197
|
+
help="使用历史的 M 块选取规则(取最后一个 M 块)",
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--legacy-profile",
|
|
201
|
+
action="store_true",
|
|
202
|
+
help="一键复现历史结果(= legacy 坐标 + legacy 归属 + 由 MD 推导错配)",
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument("--self-test", action="store_true", help="只跑内置自检(不读 BAM)")
|
|
205
|
+
parser.set_defaults(handler=run_scan)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def run_scan(args: argparse.Namespace) -> int:
|
|
209
|
+
if args.self_test:
|
|
210
|
+
scan.run_self_test()
|
|
211
|
+
return 0
|
|
212
|
+
settings = scan.ScanSettings(
|
|
213
|
+
bam=tuple(args.bam),
|
|
214
|
+
bam_dir=args.bam_dir,
|
|
215
|
+
bam_glob=args.bam_glob,
|
|
216
|
+
no_recursive=args.no_recursive,
|
|
217
|
+
out_dir=args.out_dir,
|
|
218
|
+
sample_name=args.sample_name,
|
|
219
|
+
query_chr=args.query_chr,
|
|
220
|
+
insert_chrs=args.insert_chrs,
|
|
221
|
+
insert_origin_policy=args.insert_origin_policy,
|
|
222
|
+
merge_window_bp=args.merge_window_bp,
|
|
223
|
+
min_mapq=args.min_mapq,
|
|
224
|
+
max_mismatch=args.max_mismatch,
|
|
225
|
+
max_softclip_blocks=args.max_softclip_blocks,
|
|
226
|
+
min_clip_length=args.min_clip_length,
|
|
227
|
+
keep_secondary=args.keep_secondary,
|
|
228
|
+
mismatch_source=args.mismatch_source,
|
|
229
|
+
on_missing_tag=args.on_missing_tag,
|
|
230
|
+
no_split_match=args.no_split_match,
|
|
231
|
+
no_candidate_gate=args.no_candidate_gate,
|
|
232
|
+
no_duplicate_filter=args.no_duplicate_filter,
|
|
233
|
+
require_dup_flags=args.require_dup_flags,
|
|
234
|
+
bam_threads=args.bam_threads,
|
|
235
|
+
legacy_coordinates=args.legacy_coordinates,
|
|
236
|
+
legacy_profile=args.legacy_profile,
|
|
237
|
+
log_level=args.log_level,
|
|
238
|
+
quiet=args.quiet,
|
|
239
|
+
)
|
|
240
|
+
result = scan.run(settings)
|
|
241
|
+
print(
|
|
242
|
+
f"完成:{len(result['bams'])} 个 BAM,"
|
|
243
|
+
f"query={result['query_chr']},输出目录 {result['out_dir']}"
|
|
244
|
+
)
|
|
245
|
+
return 0
|