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/plot.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""绘图子命令:``alnkit plot <ribbon|tracks>``。
|
|
2
|
+
|
|
3
|
+
参数名与旧的单文件脚本保持一一对应,迁移期只需换命令名。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
from ..core.errors import UsageError
|
|
11
|
+
from ._common import GLOBAL_OPTIONS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require_plot_dependency():
|
|
15
|
+
"""绘图子命令才需要 matplotlib;缺了要给可行动提示而不是 traceback。
|
|
16
|
+
|
|
17
|
+
基础安装(``pip install alnkit``)不含 matplotlib,所以这里必须惰性导入,
|
|
18
|
+
否则连 ``alnkit --version`` 都会在 import 阶段就崩(0.1.0 的真实缺陷)。
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
from ..plot import ribbon, tracks
|
|
22
|
+
except ImportError as exc: # pragma: no cover - 只在缺 matplotlib 时触发
|
|
23
|
+
raise UsageError(
|
|
24
|
+
f'绘图子命令需要 matplotlib({exc})。安装:pip install "alnkit[plot]"'
|
|
25
|
+
) from exc
|
|
26
|
+
return ribbon, tracks
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
30
|
+
plot = subparsers.add_parser(
|
|
31
|
+
"plot",
|
|
32
|
+
help="绘图(丝带图 / 单 read 轨道图)",
|
|
33
|
+
description="静态图输出:PNG + SVG + PDF;不依赖任何上游步骤产物。",
|
|
34
|
+
)
|
|
35
|
+
plot_subparsers = plot.add_subparsers(dest="plot_command", metavar="<subcommand>")
|
|
36
|
+
plot_subparsers.required = True
|
|
37
|
+
_add_tracks(plot_subparsers)
|
|
38
|
+
_add_ribbon(plot_subparsers)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _add_tracks(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
42
|
+
parser = subparsers.add_parser(
|
|
43
|
+
"tracks",
|
|
44
|
+
parents=[GLOBAL_OPTIONS],
|
|
45
|
+
help="单条 read 的比对片段轨道图",
|
|
46
|
+
description=(
|
|
47
|
+
"一条 read 一行,横轴为 read 自身坐标;每个比对片段画成一个块。"
|
|
48
|
+
"输入三选一:--bam / --paf / --segments-tsv。"
|
|
49
|
+
),
|
|
50
|
+
epilog=(
|
|
51
|
+
"例子:\n alnkit plot tracks --segments-tsv seg.tsv --outdir out\n"
|
|
52
|
+
" alnkit plot tracks --bam reads.bam --read readA --exog insertA "
|
|
53
|
+
"--highlight-ref chr1:1000001 --outdir out"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
source = parser.add_mutually_exclusive_group(required=True)
|
|
57
|
+
source.add_argument("--bam", help="BAM(pysam 读取)")
|
|
58
|
+
source.add_argument("--paf", help="minimap2 PAF(需要 -c)")
|
|
59
|
+
source.add_argument("--segments-tsv", dest="segments_tsv", help="已整理的片段表")
|
|
60
|
+
parser.add_argument("--read", action="append", default=[], help="只画这些 read;可重复")
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--reads-file", dest="reads_file", help="read 名列表文件(一行一个,空行/# 跳过)"
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--exog", action="append", default=[], help="外源序列名;可重复或用逗号分隔"
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--bam-mode",
|
|
69
|
+
dest="bam_mode",
|
|
70
|
+
choices=("auto", "scan", "subset"),
|
|
71
|
+
default="auto",
|
|
72
|
+
help="BAM 取数方式:auto(默认)/scan/subset",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--min-mapq", dest="min_mapq", type=int, default=0, help="最小 MAPQ(默认 0)"
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--include-secondary",
|
|
79
|
+
dest="include_secondary",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="包含 secondary 比对",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--highlight-ref",
|
|
85
|
+
dest="highlight_ref",
|
|
86
|
+
action="append",
|
|
87
|
+
default=[],
|
|
88
|
+
help="要标注的参考断点,格式 chrom:pos;可重复",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument("--outdir", required=True, help="输出目录")
|
|
91
|
+
parser.add_argument("--name", default="read_tracks", help="输出文件名前缀")
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--title", default="Read-axis alignment segments", help="图标题(英文,避免缺字体)"
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--full-read-names",
|
|
97
|
+
dest="full_read_names",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="左侧显示完整 read 名",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--dpi", type=int, default=150, help="PNG 分辨率(默认 150)")
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--fig-width", dest="fig_width", type=float, default=14.0, help="图宽(英寸,默认 14)"
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--exact-highlight",
|
|
107
|
+
dest="exact_highlight",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="参考断点按 CIGAR 精确映射(BAM 输入)",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--label-mode",
|
|
113
|
+
dest="label_mode",
|
|
114
|
+
choices=("auto", "inline", "side"),
|
|
115
|
+
default="auto",
|
|
116
|
+
help="片段标签:auto(默认)/inline/side",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--max-axis-bp",
|
|
120
|
+
dest="max_axis_bp",
|
|
121
|
+
type=int,
|
|
122
|
+
default=None,
|
|
123
|
+
help="超过该长度折叠内容空隙(断轴)",
|
|
124
|
+
)
|
|
125
|
+
parser.set_defaults(handler=run_tracks)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_tracks(args: argparse.Namespace) -> int:
|
|
129
|
+
_ribbon_unused, tracks = _require_plot_dependency()
|
|
130
|
+
settings = tracks.TrackSettings(
|
|
131
|
+
outdir=args.outdir,
|
|
132
|
+
name=args.name,
|
|
133
|
+
bam=args.bam,
|
|
134
|
+
paf=args.paf,
|
|
135
|
+
segments_tsv=args.segments_tsv,
|
|
136
|
+
read=tuple(args.read),
|
|
137
|
+
reads_file=args.reads_file,
|
|
138
|
+
exog=tuple(args.exog),
|
|
139
|
+
bam_mode=args.bam_mode,
|
|
140
|
+
min_mapq=args.min_mapq,
|
|
141
|
+
include_secondary=args.include_secondary,
|
|
142
|
+
highlight_ref=tuple(args.highlight_ref),
|
|
143
|
+
title=args.title,
|
|
144
|
+
full_read_names=args.full_read_names,
|
|
145
|
+
dpi=args.dpi,
|
|
146
|
+
fig_width=args.fig_width,
|
|
147
|
+
exact_highlight=args.exact_highlight,
|
|
148
|
+
label_mode=args.label_mode,
|
|
149
|
+
max_axis_bp=args.max_axis_bp,
|
|
150
|
+
)
|
|
151
|
+
result = tracks.run(settings)
|
|
152
|
+
print(f"reads: {result['reads']};segments: {result['segments']}")
|
|
153
|
+
for key, label in (
|
|
154
|
+
("png", "图"),
|
|
155
|
+
("svg", "SVG"),
|
|
156
|
+
("pdf", "PDF"),
|
|
157
|
+
("segments_tsv", "片段表"),
|
|
158
|
+
):
|
|
159
|
+
print(f"{label}: {result[key]}")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _add_ribbon(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
164
|
+
parser = subparsers.add_parser(
|
|
165
|
+
"ribbon",
|
|
166
|
+
parents=[GLOBAL_OPTIONS],
|
|
167
|
+
help="共线性丝带图(参考 vs 多个查询片段)",
|
|
168
|
+
description=(
|
|
169
|
+
"方式 1:给 --reference + --query,内部跑 minimap2;"
|
|
170
|
+
"方式 2:给 --paf(现成比对),直接画图。"
|
|
171
|
+
),
|
|
172
|
+
epilog=(
|
|
173
|
+
"例子:\n alnkit plot ribbon --paf aln.paf --reference-name assembly_a "
|
|
174
|
+
"--outdir out --name ex4\n"
|
|
175
|
+
' alnkit plot ribbon --reference "ins=exog.fa:insertA" '
|
|
176
|
+
'--query "C01=asm.fa:oe_contig1" --outdir out --name exog_vs_class'
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"--reference", help="参考侧,格式 标签=fasta路径[:片段名](--paf 模式可省略)"
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument("--query", action="append", default=[], help="查询侧,格式同上;可重复")
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--query-file", dest="query_file", help="查询清单文件(每行一个 标签=路径[:片段名])"
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument("--paf", help="现成 PAF(方式 2;需带 -c 才有匹配列)")
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--reference-name", dest="reference_name", help="--paf 模式下指定哪条是参考行"
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--query-name",
|
|
192
|
+
dest="query_name",
|
|
193
|
+
action="append",
|
|
194
|
+
default=[],
|
|
195
|
+
help="只画这些查询片段;可重复",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument("--outdir", required=True, help="输出目录")
|
|
198
|
+
parser.add_argument("--name", required=True, help="输出文件名前缀")
|
|
199
|
+
parser.add_argument("--title", default="", help="图标题(英文,避免缺字体)")
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--reference-position",
|
|
202
|
+
dest="reference_position",
|
|
203
|
+
choices=("top", "bottom"),
|
|
204
|
+
default="top",
|
|
205
|
+
help="参考行放顶部还是底部",
|
|
206
|
+
)
|
|
207
|
+
order = parser.add_mutually_exclusive_group()
|
|
208
|
+
order.add_argument("--order", help="行顺序,用 > 分隔")
|
|
209
|
+
order.add_argument("--order-file", dest="order_file", help="行顺序清单文件")
|
|
210
|
+
parser.add_argument("--dpi", type=int, default=100, help="PNG 分辨率(默认 100)")
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"--min-label-bp",
|
|
213
|
+
dest="min_label_bp",
|
|
214
|
+
type=int,
|
|
215
|
+
default=300,
|
|
216
|
+
help="只标长度不小于它的块(默认 300)",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--label-coordinate",
|
|
220
|
+
dest="label_coordinate",
|
|
221
|
+
choices=("query", "reference", "both"),
|
|
222
|
+
default="query",
|
|
223
|
+
help="标签用哪套坐标",
|
|
224
|
+
)
|
|
225
|
+
parser.add_argument(
|
|
226
|
+
"--max-labels-per-row",
|
|
227
|
+
dest="max_labels_per_row",
|
|
228
|
+
type=int,
|
|
229
|
+
default=8,
|
|
230
|
+
help="每行最多标签数(默认 8)",
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument("--placement-table", dest="placement_table", help="额外输出位置汇总表")
|
|
233
|
+
parser.add_argument("--region-table", dest="region_table", help="额外输出同源区域表")
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--min-identity",
|
|
236
|
+
dest="min_identity",
|
|
237
|
+
type=float,
|
|
238
|
+
default=0.8,
|
|
239
|
+
help="保留块的最小一致率(默认 0.8)",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--min-block-bp",
|
|
243
|
+
dest="min_block_bp",
|
|
244
|
+
type=int,
|
|
245
|
+
default=200,
|
|
246
|
+
help="保留块的最小长度(默认 200)",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--preset", default="asm20", help="minimap2 预设(默认 asm20;auto=按长度分档)"
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
"--query-preset",
|
|
253
|
+
dest="query_preset",
|
|
254
|
+
action="append",
|
|
255
|
+
default=[],
|
|
256
|
+
metavar="标签=预设[ 额外参数]",
|
|
257
|
+
help="按查询标签覆盖比对参数;可重复",
|
|
258
|
+
)
|
|
259
|
+
parser.add_argument("--threads", type=int, default=8, help="线程数(默认 8)")
|
|
260
|
+
parser.set_defaults(handler=run_ribbon)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_ribbon(args: argparse.Namespace) -> int:
|
|
264
|
+
ribbon, _tracks_unused = _require_plot_dependency()
|
|
265
|
+
settings = ribbon.RibbonSettings(
|
|
266
|
+
outdir=args.outdir,
|
|
267
|
+
name=args.name,
|
|
268
|
+
reference=args.reference,
|
|
269
|
+
query=tuple(args.query),
|
|
270
|
+
query_file=args.query_file,
|
|
271
|
+
paf=args.paf,
|
|
272
|
+
reference_name=args.reference_name,
|
|
273
|
+
query_name=tuple(args.query_name),
|
|
274
|
+
title=args.title,
|
|
275
|
+
reference_position=args.reference_position,
|
|
276
|
+
order=args.order,
|
|
277
|
+
order_file=args.order_file,
|
|
278
|
+
dpi=args.dpi,
|
|
279
|
+
min_label_bp=args.min_label_bp,
|
|
280
|
+
label_coordinate=args.label_coordinate,
|
|
281
|
+
max_labels_per_row=args.max_labels_per_row,
|
|
282
|
+
placement_table=args.placement_table,
|
|
283
|
+
region_table=args.region_table,
|
|
284
|
+
min_identity=args.min_identity,
|
|
285
|
+
min_block_bp=args.min_block_bp,
|
|
286
|
+
preset=args.preset,
|
|
287
|
+
query_preset=tuple(args.query_preset),
|
|
288
|
+
threads=args.threads,
|
|
289
|
+
)
|
|
290
|
+
result = ribbon.run(settings)
|
|
291
|
+
print(f"图: {result['png']}")
|
|
292
|
+
print(f"映射块表: {result['blocks_tsv']}")
|
|
293
|
+
return 0
|
alnkit/cli/ref.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""``alnkit ref <merge>``:参考准备(A1)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from .. import prepare
|
|
8
|
+
from ._common import GLOBAL_OPTIONS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
12
|
+
ref = subparsers.add_parser(
|
|
13
|
+
"ref",
|
|
14
|
+
help="参考准备(合并外源序列)",
|
|
15
|
+
description="把参考基因组与外源序列合成一条合并参考,供后续比对使用。",
|
|
16
|
+
)
|
|
17
|
+
sub = ref.add_subparsers(dest="ref_command", metavar="<subcommand>")
|
|
18
|
+
sub.required = True
|
|
19
|
+
|
|
20
|
+
parser = sub.add_parser(
|
|
21
|
+
"merge",
|
|
22
|
+
parents=[GLOBAL_OPTIONS],
|
|
23
|
+
help="参考 + 外源 -> merged_reference.fa(含 .fai 与 header 表)",
|
|
24
|
+
description=(
|
|
25
|
+
"外源按顺序转大写后拼到参考末尾;同时输出 reference.fa(.fai)、"
|
|
26
|
+
"reference_headers.tsv、exogenous_uppercased.fa。"
|
|
27
|
+
),
|
|
28
|
+
epilog=(
|
|
29
|
+
"例子:\n alnkit ref merge --reference ref.fa.gz \\\n"
|
|
30
|
+
" --exog exog/insertA.fa --exog exog/insertB.fa --outdir 01_merge_ref"
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("--reference", required=True, help="参考基因组 FASTA(可 .gz)")
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--exog",
|
|
36
|
+
action="append",
|
|
37
|
+
default=[],
|
|
38
|
+
required=True,
|
|
39
|
+
help="外源 FASTA,可重复(按给出顺序拼接)",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("--outdir", required=True, help="输出目录")
|
|
42
|
+
parser.set_defaults(handler=run_merge)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_merge(args: argparse.Namespace) -> int:
|
|
46
|
+
result = prepare.merge_reference(
|
|
47
|
+
prepare.MergeSettings(reference=args.reference, exog=tuple(args.exog), outdir=args.outdir)
|
|
48
|
+
)
|
|
49
|
+
print(f"完成:{result['sequences']} 条序列 -> {result['merged']}")
|
|
50
|
+
return 0
|
alnkit/cli/seg.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""``alnkit seg table``:BAM -> 比对段表(D2 的 CLI 出口)。
|
|
2
|
+
|
|
3
|
+
默认写**规范列**(带基准后缀);``--columns legacy`` 写历史上那张 14 列表,
|
|
4
|
+
用于与旧脚本逐字节对账。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from ..core import tables
|
|
13
|
+
from ..core.bam_segments import load_alignment_segments, load_fai_lengths
|
|
14
|
+
from ..core.errors import UsageError
|
|
15
|
+
from ..core.segments import Segment, segment_rows
|
|
16
|
+
from ._common import GLOBAL_OPTIONS
|
|
17
|
+
|
|
18
|
+
#: 历史 14 列表:对外列名 -> Segment 字段(顺序不可动,黄金对账依赖它)
|
|
19
|
+
LEGACY_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
20
|
+
("chrom", "chrom"),
|
|
21
|
+
("chrom_length", "chrom_length"),
|
|
22
|
+
("ref_pos_start", "ref_start_0based"),
|
|
23
|
+
("ref_pos_end", "ref_end_0based"),
|
|
24
|
+
("read_name", "read_name"),
|
|
25
|
+
("read_pos_start", "read_start_0based"),
|
|
26
|
+
("read_pos_end", "read_end_0based"),
|
|
27
|
+
("strand", "strand"),
|
|
28
|
+
("mapping_quality", "mapq"),
|
|
29
|
+
("is_secondary", "is_secondary"),
|
|
30
|
+
("is_supplementary", "is_supplementary"),
|
|
31
|
+
("ref_aln_len", "ref_aln_len"),
|
|
32
|
+
("read_aln_len", "read_aln_len"),
|
|
33
|
+
("simplified_cigar", "cigar"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
38
|
+
seg = subparsers.add_parser(
|
|
39
|
+
"seg",
|
|
40
|
+
help="比对段表(BAM -> TSV)",
|
|
41
|
+
description="把 BAM 的每条比对导成比对段表;坐标 0-based 半开,read 轴已翻正。",
|
|
42
|
+
)
|
|
43
|
+
seg_subparsers = seg.add_subparsers(dest="seg_command", metavar="<subcommand>")
|
|
44
|
+
seg_subparsers.required = True
|
|
45
|
+
_add_table(seg_subparsers)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _add_table(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
49
|
+
parser = subparsers.add_parser(
|
|
50
|
+
"table",
|
|
51
|
+
parents=[GLOBAL_OPTIONS],
|
|
52
|
+
help="BAM -> 比对段表",
|
|
53
|
+
description="输出规范列(默认)或历史 14 列(--columns legacy)。",
|
|
54
|
+
epilog=(
|
|
55
|
+
"例子:\n alnkit seg table --bam reads.sorted.bam --fai ref.fa.fai --out seg.tsv\n"
|
|
56
|
+
" alnkit seg table --bam reads.bam --out legacy.tsv --columns legacy"
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument("--bam", required=True, help="比对 BAM(顺序扫描,不需要索引)")
|
|
60
|
+
parser.add_argument("--out", required=True, help="输出 TSV")
|
|
61
|
+
parser.add_argument("--fai", default="", help="参考序列的 .fai(提供后带 chrom_length 列)")
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--columns",
|
|
64
|
+
choices=("canonical", "legacy"),
|
|
65
|
+
default="canonical",
|
|
66
|
+
help="canonical(默认,带基准后缀)或 legacy(历史 14 列,逐字节兼容)",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument("--read-file", dest="read_file", help="只导出这些 read(一行一个)")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--skip-secondary", dest="skip_secondary", action="store_true", help="跳过 secondary 比对"
|
|
71
|
+
)
|
|
72
|
+
parser.set_defaults(handler=run_table)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _format_legacy(segment: Segment) -> list[str]:
|
|
76
|
+
values: list[str] = []
|
|
77
|
+
for _, field in LEGACY_COLUMNS:
|
|
78
|
+
value = getattr(segment, field)
|
|
79
|
+
if value is None:
|
|
80
|
+
values.append("-")
|
|
81
|
+
elif isinstance(value, bool):
|
|
82
|
+
values.append("1" if value else "0")
|
|
83
|
+
else:
|
|
84
|
+
values.append(str(value))
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def write_legacy_table(segments: list[Segment], path: str) -> int:
|
|
89
|
+
"""写历史 14 列表(列名/列序/排序/取值与旧脚本一致)。"""
|
|
90
|
+
ordered = sorted(segments, key=lambda s: (s.read_name, s.chrom, s.ref_start_0based))
|
|
91
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
92
|
+
os.makedirs(directory, exist_ok=True)
|
|
93
|
+
with open(path, "w", encoding="utf-8", newline="") as handle:
|
|
94
|
+
handle.write("\t".join(name for name, _ in LEGACY_COLUMNS) + "\n")
|
|
95
|
+
for segment in ordered:
|
|
96
|
+
handle.write("\t".join(_format_legacy(segment)) + "\n")
|
|
97
|
+
return len(ordered)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_table(args: argparse.Namespace) -> int:
|
|
101
|
+
chrom_lengths = load_fai_lengths(args.fai) if args.fai else None
|
|
102
|
+
selected = None
|
|
103
|
+
if args.read_file:
|
|
104
|
+
with open(args.read_file, encoding="utf-8") as handle:
|
|
105
|
+
selected = {
|
|
106
|
+
line.strip() for line in handle if line.strip() and not line.startswith("#")
|
|
107
|
+
}
|
|
108
|
+
if not selected:
|
|
109
|
+
raise UsageError(f"{args.read_file} 里没有 read 名")
|
|
110
|
+
|
|
111
|
+
segments = load_alignment_segments(
|
|
112
|
+
args.bam,
|
|
113
|
+
chrom_lengths=chrom_lengths,
|
|
114
|
+
selected_read_names=selected,
|
|
115
|
+
skip_secondary=args.skip_secondary,
|
|
116
|
+
)
|
|
117
|
+
if args.columns == "legacy":
|
|
118
|
+
count = write_legacy_table(segments, args.out)
|
|
119
|
+
else:
|
|
120
|
+
count = tables.write_tsv(
|
|
121
|
+
args.out,
|
|
122
|
+
"segments",
|
|
123
|
+
segment_rows(segments),
|
|
124
|
+
sort_key=lambda row: (
|
|
125
|
+
row["read_name"],
|
|
126
|
+
row["chrom"],
|
|
127
|
+
row["ref_start_0based"],
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
print(f"比对表 {args.out}:{count} 行(columns={args.columns})")
|
|
131
|
+
return 0
|