alnkit 0.1.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.

Potentially problematic release.


This version of alnkit might be problematic. Click here for more details.

Files changed (49) hide show
  1. alnkit/__init__.py +11 -0
  2. alnkit/__main__.py +10 -0
  3. alnkit/_docs.py +52 -0
  4. alnkit/cli/__init__.py +53 -0
  5. alnkit/cli/__main__.py +10 -0
  6. alnkit/cli/_common.py +25 -0
  7. alnkit/cli/align.py +52 -0
  8. alnkit/cli/doctor.py +81 -0
  9. alnkit/cli/long.py +346 -0
  10. alnkit/cli/plot.py +293 -0
  11. alnkit/cli/ref.py +50 -0
  12. alnkit/cli/seg.py +131 -0
  13. alnkit/cli/selftest.py +308 -0
  14. alnkit/cli/short.py +245 -0
  15. alnkit/core/__init__.py +9 -0
  16. alnkit/core/bam_segments.py +183 -0
  17. alnkit/core/coords.py +129 -0
  18. alnkit/core/errors.py +25 -0
  19. alnkit/core/external.py +63 -0
  20. alnkit/core/fasta.py +83 -0
  21. alnkit/core/logging.py +25 -0
  22. alnkit/core/paf.py +52 -0
  23. alnkit/core/segments.py +253 -0
  24. alnkit/core/tables.py +332 -0
  25. alnkit/longread/__init__.py +5 -0
  26. alnkit/longread/assemble.py +453 -0
  27. alnkit/longread/breakpoints.py +910 -0
  28. alnkit/longread/classify.py +958 -0
  29. alnkit/longread/graph.py +174 -0
  30. alnkit/longread/locate.py +904 -0
  31. alnkit/plot/__init__.py +5 -0
  32. alnkit/plot/ribbon.py +1306 -0
  33. alnkit/plot/tracks.py +1035 -0
  34. alnkit/prepare.py +233 -0
  35. alnkit/py.typed +0 -0
  36. alnkit/shortread/__init__.py +20 -0
  37. alnkit/shortread/bam.py +527 -0
  38. alnkit/shortread/evidence.py +214 -0
  39. alnkit/shortread/interference.py +44 -0
  40. alnkit/shortread/merge.py +126 -0
  41. alnkit/shortread/model.py +301 -0
  42. alnkit/shortread/origin.py +507 -0
  43. alnkit/shortread/report.py +240 -0
  44. alnkit/shortread/scan.py +763 -0
  45. alnkit-0.1.0.dist-info/METADATA +165 -0
  46. alnkit-0.1.0.dist-info/RECORD +49 -0
  47. alnkit-0.1.0.dist-info/WHEEL +4 -0
  48. alnkit-0.1.0.dist-info/entry_points.txt +2 -0
  49. alnkit-0.1.0.dist-info/licenses/LICENSE +674 -0
alnkit/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """alnkit:比对结果中外源插入(及未来结构变异)的证据提取与可视化。
2
+
3
+ 已迁入老三套流程的全部命令:三代(A1–A7)、二代(B1/B2)、绘图(C1/C2),
4
+ 并提供坐标/表契约层与 `alnkit selftest`。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ __all__ = ["__version__"]
alnkit/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """``python -m alnkit`` 等价于 ``alnkit``。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
alnkit/_docs.py ADDED
@@ -0,0 +1,52 @@
1
+ """文档生成:表契约 → markdown(唯一来源是 ``core.tables``)。
2
+
3
+ 放成私有模块(而不是 ``scripts/`` 里的独立脚本),是为了让测试能直接 import 并校验
4
+ “文档与代码一致”,不必做 sys.path 手脚。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .core import tables
10
+
11
+ _HEADER = """# 表契约
12
+
13
+ > 本页由 `alnkit.core.tables` 的 `COLUMNS` / `COLUMN_DOC` 生成,**不要手改**。
14
+ > 重新生成:`python scripts/gen_table_docs.py`。
15
+
16
+ 规则:
17
+
18
+ - 列序即下表顺序,写表只能通过 `alnkit.core.tables.write_tsv`;
19
+ - 坐标列名必须带 `_0based` / `_1based` 后缀;长度列(`*_bp`、`*_len`、`*_length`、`*_count`)不带;
20
+ - 旧表(legacy `.xls` / 旧 TSV)列名保持不变,由 `--columns legacy:*` 复现;
21
+ - 浮点固定 4 位小数;空值写 `-`。
22
+ """
23
+
24
+
25
+ #: 注册了但还没有生产者的表(文档里要写明,避免用户去找不存在的文件)
26
+ NOT_PRODUCED = frozenset({"insert_sites"})
27
+
28
+
29
+ def render_table_docs() -> str:
30
+ """渲染 docs/concepts/tables.md 的完整内容。"""
31
+ chunks = [_HEADER]
32
+ for table, columns in tables.COLUMNS.items():
33
+ docs = tables.COLUMN_DOC.get(table, {})
34
+ chunks.append(f"\n## `{table}`\n")
35
+ if table in NOT_PRODUCED:
36
+ chunks.append(
37
+ '!!! warning "尚未实现"\n'
38
+ " 0.1.0 还没有命令输出这张表;列定义先冻结在这里,计划 0.2 接上。\n"
39
+ )
40
+ chunks.append("| # | 列名 | 基准 | 说明 |")
41
+ chunks.append("|---|---|---|---|")
42
+ for index, column in enumerate(columns, start=1):
43
+ if column.endswith("_0based"):
44
+ basis = "0-based"
45
+ elif column.endswith("_1based"):
46
+ basis = "1-based"
47
+ elif tables.is_coordinate_column(column): # pragma: no cover - 注册时已拦住
48
+ basis = "**缺失**"
49
+ else:
50
+ basis = "—"
51
+ chunks.append(f"| {index} | `{column}` | {basis} | {docs.get(column, '')} |")
52
+ return "\n".join(chunks) + "\n"
alnkit/cli/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """命令行入口:``alnkit <command>``。
2
+
3
+ ``doctor`` / ``selftest`` / ``plot tracks`` 已可用;其余分析命令随 P1–P3 迁入。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from collections.abc import Sequence
11
+
12
+ from .. import __version__
13
+ from ..core import errors
14
+ from . import align, doctor, long, plot, ref, seg, selftest, short
15
+ from ._common import apply_global_options
16
+
17
+ _COMMANDS = (doctor, selftest, ref, align, seg, plot, short, long)
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ """构建完整命令树(供 CLI 与文档生成共用)。"""
22
+ parser = argparse.ArgumentParser(
23
+ prog="alnkit",
24
+ description="比对结果中外源插入(及未来结构变异)的证据提取与可视化工具集",
25
+ epilog="例子:alnkit doctor | alnkit selftest | alnkit <command> --help",
26
+ )
27
+ parser.add_argument("--version", action="version", version=f"alnkit {__version__}")
28
+ subparsers = parser.add_subparsers(dest="command", metavar="<command>")
29
+ for module in _COMMANDS:
30
+ module.add_parser(subparsers)
31
+ return parser
32
+
33
+
34
+ def main(argv: Sequence[str] | None = None) -> int:
35
+ """CLI 主入口;返回进程退出码。"""
36
+ parser = build_parser()
37
+ args = parser.parse_args(argv)
38
+ if getattr(args, "command", None) is None:
39
+ parser.print_help()
40
+ return 0
41
+ apply_global_options(args)
42
+ try:
43
+ return int(args.handler(args))
44
+ except errors.AlnkitError as exc:
45
+ print(f"alnkit {args.command}: {exc}", file=sys.stderr)
46
+ return exc.exit_code
47
+ except KeyboardInterrupt:
48
+ print("已中断", file=sys.stderr)
49
+ return 130
50
+
51
+
52
+ if __name__ == "__main__":
53
+ sys.exit(main())
alnkit/cli/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """``python -m alnkit.cli`` 等价于 ``alnkit``(便于在子进程里调用,不必依赖 PATH)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from . import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
alnkit/cli/_common.py ADDED
@@ -0,0 +1,25 @@
1
+ """CLI 公共件:解析器骨架、全局参数、统一错误出口。
2
+
3
+ 参数命名规则:kebab-case 长参数,尽量与旧脚本同名(`--min-mapq`、`--merge-window-bp`…),
4
+ 迁移期用户只需换命令名。退出码:0 成功 / 1 运行期错误 / 2 参数或前置校验失败。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+
11
+ from ..core.logging import configure
12
+
13
+ GLOBAL_OPTIONS = argparse.ArgumentParser(add_help=False)
14
+ GLOBAL_OPTIONS.add_argument(
15
+ "--log-level",
16
+ default="INFO",
17
+ choices=("DEBUG", "INFO", "WARNING", "ERROR"),
18
+ help="日志级别(默认 INFO)",
19
+ )
20
+ GLOBAL_OPTIONS.add_argument("--quiet", action="store_true", help="只输出错误")
21
+
22
+
23
+ def apply_global_options(args: argparse.Namespace) -> None:
24
+ """把全局参数应用到日志系统。"""
25
+ configure(level=getattr(args, "log_level", "INFO"), quiet=getattr(args, "quiet", False))
alnkit/cli/align.py ADDED
@@ -0,0 +1,52 @@
1
+ """``alnkit align <reads>``:把 reads 比对到合并参考(A2)。"""
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
+ align = subparsers.add_parser(
13
+ "align",
14
+ help="比对(reads -> 合并参考)",
15
+ description="调用 minimap2 + samtools 的薄封装;需要它们装在 PATH 里。",
16
+ )
17
+ sub = align.add_subparsers(dest="align_command", metavar="<subcommand>")
18
+ sub.required = True
19
+
20
+ parser = sub.add_parser(
21
+ "reads",
22
+ parents=[GLOBAL_OPTIONS],
23
+ help="长读长 reads -> sorted BAM(-ax map-hifi -Y -N 50)",
24
+ description=(
25
+ "等价于旧 shell:minimap2 建 .mmi -> minimap2 -ax map-hifi -Y -N 50 -> "
26
+ "samtools sort -> samtools index。"
27
+ ),
28
+ epilog=(
29
+ "例子:alnkit align reads --reference 01_merge_ref/merged_reference.fa \\\n"
30
+ " --reads input.fastq --outdir 02_align --threads 12"
31
+ ),
32
+ )
33
+ parser.add_argument("--reference", required=True, help="合并参考 FASTA(需 .fai)")
34
+ parser.add_argument("--reads", required=True, help="原始 reads(FASTQ/FASTA)")
35
+ parser.add_argument("--outdir", required=True, help="输出目录")
36
+ parser.add_argument("--name", default="reads_to_merged", help="输出 BAM 文件名前缀")
37
+ parser.add_argument("--threads", type=int, default=12, help="线程数(默认 12)")
38
+ parser.set_defaults(handler=run_align)
39
+
40
+
41
+ def run_align(args: argparse.Namespace) -> int:
42
+ result = prepare.align_reads(
43
+ prepare.AlignSettings(
44
+ reference=args.reference,
45
+ reads=args.reads,
46
+ outdir=args.outdir,
47
+ name=args.name,
48
+ threads=args.threads,
49
+ )
50
+ )
51
+ print(f"完成:{result['bam']}")
52
+ return 0
alnkit/cli/doctor.py ADDED
@@ -0,0 +1,81 @@
1
+ """``alnkit doctor``:打印运行环境,缺什么就说什么。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import platform
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ from .. import __version__
10
+ from ..core import errors
11
+ from ..core.external import ALIGNERS, REFERENCE_TOOLS, find_tool
12
+ from ._common import GLOBAL_OPTIONS
13
+
14
+ _REQUIRED_PACKAGES = ("pysam",)
15
+ _OPTIONAL_PACKAGES = (("matplotlib", "绘图子命令 alnkit plot *"),)
16
+
17
+
18
+ def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
19
+ parser = subparsers.add_parser(
20
+ "doctor",
21
+ parents=[GLOBAL_OPTIONS],
22
+ help="检查运行环境(Python / 依赖 / 外部命令)",
23
+ description="打印版本与可用性;必需项缺失时以退出码 1 结束。",
24
+ epilog="例子:alnkit doctor",
25
+ )
26
+ parser.set_defaults(handler=run)
27
+
28
+
29
+ def _package_version(name: str) -> str | None:
30
+ try:
31
+ return version(name)
32
+ except PackageNotFoundError:
33
+ return None
34
+
35
+
36
+ def run(args: argparse.Namespace) -> int:
37
+ del args # doctor 不使用全局参数以外的入参
38
+ rows: list[tuple[str, str, str]] = []
39
+ rows.append(("alnkit", __version__, "必需"))
40
+ rows.append(("python", platform.python_version(), ">=3.10"))
41
+
42
+ missing_required: list[str] = []
43
+ for name in _REQUIRED_PACKAGES:
44
+ found = _package_version(name)
45
+ rows.append((name, found or "缺失", "必需"))
46
+ if found is None:
47
+ missing_required.append(name)
48
+
49
+ missing_optional: list[str] = []
50
+ for name, purpose in _OPTIONAL_PACKAGES:
51
+ found = _package_version(name)
52
+ rows.append((name, found or "缺失", f"可选:{purpose}"))
53
+ if found is None:
54
+ missing_optional.append(name)
55
+
56
+ for name in ALIGNERS:
57
+ tool = find_tool(name)
58
+ rows.append((name, tool.describe(), "可选:比对(至少装一个)"))
59
+ for name in REFERENCE_TOOLS:
60
+ tool = find_tool(name)
61
+ rows.append((name, tool.describe(), "可选:参考索引 / BAM 处理"))
62
+
63
+ width = max(len(name) for name, _, _ in rows)
64
+ print(f"alnkit {__version__} 平台 {platform.platform()}")
65
+ print(f"{'组件':<{width}} 版本{'':<38}说明")
66
+ for name, value, note in rows:
67
+ print(f"{name:<{width}} {value:<40}{note}")
68
+
69
+ aligners = [name for name in ALIGNERS if find_tool(name).available]
70
+ if missing_required:
71
+ raise errors.RunError(
72
+ "缺少必需依赖:" + ", ".join(missing_required) + "。安装:pip install alnkit"
73
+ )
74
+ if missing_optional:
75
+ print(
76
+ f"\n提示:未安装 {', '.join(missing_optional)};"
77
+ '绘图子命令需要 `pip install "alnkit[plot]"`。'
78
+ )
79
+ if not aligners:
80
+ print(f"\n警告:{'/'.join(ALIGNERS)} 都未找到;分析链路需要用户自行提供 BAM/PAF。")
81
+ return 0
alnkit/cli/long.py ADDED
@@ -0,0 +1,346 @@
1
+ """长读长子命令:``alnkit long <classify|graph|breakpoints|assemble|locate>``。
2
+
3
+ 目前只有 ``classify``(A3)从旧流程迁入;其余随 P3 补齐。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+
10
+ from ..longread import assemble, breakpoints, classify, graph, locate
11
+ from ._common import GLOBAL_OPTIONS
12
+
13
+
14
+ def add_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
15
+ long = subparsers.add_parser(
16
+ "long",
17
+ help="长读长链路(三代:断点 / 类 / 组装 / 落点)",
18
+ description="PacBio/ONT 长读长上检测外源插入的结构:先分类,再建断点图与类。",
19
+ )
20
+ long_subparsers = long.add_subparsers(dest="long_command", metavar="<subcommand>")
21
+ long_subparsers.required = True
22
+ _add_graph(long_subparsers)
23
+ _add_classify(long_subparsers)
24
+ _add_breakpoints(long_subparsers)
25
+ _add_assemble(long_subparsers)
26
+ _add_locate(long_subparsers)
27
+
28
+
29
+ def _add_classify(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
30
+ parser = subparsers.add_parser(
31
+ "classify",
32
+ parents=[GLOBAL_OPTIONS],
33
+ help="目标 reads 分类 + 接头事件/位点/区域聚类",
34
+ description=(
35
+ "先按外源证据筛出目标 read,再逐段判定外源/基因组/可疑错配,"
36
+ "最后把接头事件聚成位点与区域,并导出相关 reads 子集。"
37
+ ),
38
+ epilog=(
39
+ "例子:alnkit long classify --bam reads_to_merged.sorted.bam \\\n"
40
+ " --reads input.fastq --merged-fai merged.fa.fai \\\n"
41
+ " --exog h301_h --exog h301_l --outdir 03_classify"
42
+ ),
43
+ )
44
+ parser.add_argument("--bam", required=True, help="reads -> 合并参考 的 BAM")
45
+ parser.add_argument("--reads", required=True, help="原始 fastq")
46
+ parser.add_argument(
47
+ "--merged-fai", dest="merged_fai", required=True, help="合并参考 .fai(取序列长度)"
48
+ )
49
+ parser.add_argument(
50
+ "--exog", action="append", default=[], required=True, help="外源序列名,可重复"
51
+ )
52
+ parser.add_argument("--outdir", required=True, help="输出目录")
53
+ parser.add_argument(
54
+ "--min-mapq", dest="min_mapq", type=int, default=20, help="比对质量下限(默认 20)"
55
+ )
56
+ parser.add_argument(
57
+ "--min-exog-block-bp",
58
+ dest="min_exog_block_bp",
59
+ type=int,
60
+ default=100,
61
+ help="外源比对段最小长度(默认 100 bp)",
62
+ )
63
+ parser.add_argument(
64
+ "--contig-edge-bp",
65
+ dest="contig_edge_bp",
66
+ type=int,
67
+ default=100,
68
+ help="contig 两端多近算边缘(默认 100 bp)",
69
+ )
70
+ parser.add_argument(
71
+ "--short-genome-block-bp",
72
+ dest="short_genome_block_bp",
73
+ type=int,
74
+ default=500,
75
+ help="边缘处基因组段多短算可疑错配(默认 500 bp)",
76
+ )
77
+ parser.add_argument(
78
+ "--junction-max-gap-bp",
79
+ dest="junction_max_gap_bp",
80
+ type=int,
81
+ default=10_000,
82
+ help="基因组段与外源段在 read 轴上多近算相邻(默认 10 kb)",
83
+ )
84
+ parser.add_argument(
85
+ "--site-gap-bp",
86
+ dest="site_gap_bp",
87
+ type=int,
88
+ default=20,
89
+ help="同一位点的边界允许错位(默认 20 bp)",
90
+ )
91
+ parser.add_argument(
92
+ "--region-gap-bp",
93
+ dest="region_gap_bp",
94
+ type=int,
95
+ default=200_000,
96
+ help="同染色体位点归为同一区域的最大间距(默认 200 kb)",
97
+ )
98
+ parser.set_defaults(handler=run_classify)
99
+
100
+
101
+ def run_classify(args: argparse.Namespace) -> int:
102
+ settings = classify.ClassifySettings(
103
+ bam=args.bam,
104
+ reads=args.reads,
105
+ merged_fai=args.merged_fai,
106
+ exog=tuple(args.exog),
107
+ outdir=args.outdir,
108
+ min_mapq=args.min_mapq,
109
+ min_exog_block_bp=args.min_exog_block_bp,
110
+ contig_edge_bp=args.contig_edge_bp,
111
+ short_genome_block_bp=args.short_genome_block_bp,
112
+ junction_max_gap_bp=args.junction_max_gap_bp,
113
+ site_gap_bp=args.site_gap_bp,
114
+ region_gap_bp=args.region_gap_bp,
115
+ )
116
+ result = classify.run(settings)
117
+ print(
118
+ f"完成:总 reads {result['total_reads']},目标 {result['target_reads']},"
119
+ f"位点 {result['sites']},区域 {result['regions']};输出 {result['outdir']}"
120
+ )
121
+ return 0
122
+
123
+
124
+ def _add_breakpoints(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
125
+ parser = subparsers.add_parser(
126
+ "breakpoints",
127
+ parents=[GLOBAL_OPTIONS],
128
+ help="断点图方案 A:紧邻片段断点 + 共享 reads 合并成类",
129
+ description=(
130
+ "在 read 轴上按“紧邻片段”定义断点(基因组–外源 = vector S##;"
131
+ "基因组–基因组 = genome G##a/G##b),两个断点共享 reads >= --min-shared 就合并为一类。"
132
+ ),
133
+ epilog=(
134
+ "例子:alnkit long breakpoints \\\n"
135
+ " --segments 03_classify/alignment_segments.tsv \\\n"
136
+ " --classification 03_classify/read_classification.tsv \\\n"
137
+ " --overlaps 04_overlap/overlap_edges.tsv \\\n"
138
+ " --reads 03_classify/related_reads.fastq \\\n"
139
+ " --merged-fai merged.fa.fai --outdir 05_breakpoints"
140
+ ),
141
+ )
142
+ parser.add_argument("--segments", required=True, help="alignment_segments.tsv")
143
+ parser.add_argument("--classification", required=True, help="read_classification.tsv")
144
+ parser.add_argument("--overlaps", required=True, help="overlap_edges.tsv")
145
+ parser.add_argument("--reads", required=True, help="related_reads.fastq(输出每类子集用)")
146
+ parser.add_argument(
147
+ "--merged-fai", dest="merged_fai", required=True, help="合并参考 .fai(取序列长度)"
148
+ )
149
+ parser.add_argument("--outdir", required=True, help="输出目录(建议 05_breakpoints)")
150
+ parser.add_argument(
151
+ "--site-gap",
152
+ dest="site_gap",
153
+ type=int,
154
+ default=20,
155
+ help="同一 site 的边界允许错位(默认 20 bp)",
156
+ )
157
+ parser.add_argument(
158
+ "--min-shared",
159
+ dest="min_shared",
160
+ type=int,
161
+ default=2,
162
+ help="合并断点所需最少共享 reads(默认 2)",
163
+ )
164
+ parser.set_defaults(handler=run_breakpoints)
165
+
166
+
167
+ def run_breakpoints(args: argparse.Namespace) -> int:
168
+ settings = breakpoints.BreakpointSettings(
169
+ segments=args.segments,
170
+ classification=args.classification,
171
+ overlaps=args.overlaps,
172
+ reads=args.reads,
173
+ merged_fai=args.merged_fai,
174
+ outdir=args.outdir,
175
+ site_gap=args.site_gap,
176
+ min_shared=args.min_shared,
177
+ )
178
+ result = breakpoints.run(settings)
179
+ print(
180
+ f"完成:断点 {result['sites']} 个,类 {result['classes']} 个,"
181
+ f"segment_ends {result['segment_ends']} 行;输出 {result['outdir']}"
182
+ )
183
+ return 0
184
+
185
+
186
+ def _add_assemble(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
187
+ parser = subparsers.add_parser(
188
+ "assemble",
189
+ parents=[GLOBAL_OPTIONS],
190
+ help="按 overlap 关系贪心延伸组装每个类",
191
+ description=(
192
+ "每个类先拿一条最长的接头 read 作种子,再从 overlap 边里反复挑一条能往"
193
+ "左/右延伸的 read,把非重叠悬垂序列接到 contig 两端。"
194
+ ),
195
+ epilog=(
196
+ "例子:alnkit long assemble \\\n"
197
+ " --classes-dir 05_breakpoints/classes \\\n"
198
+ " --paf 04_overlap/overlaps.paf --outdir 06_assembly"
199
+ ),
200
+ )
201
+ parser.add_argument(
202
+ "--classes-dir", dest="classes_dir", required=True, help="05_breakpoints/classes"
203
+ )
204
+ parser.add_argument("--paf", required=True, help="all-vs-all PAF")
205
+ parser.add_argument("--outdir", required=True, help="输出目录")
206
+ parser.add_argument(
207
+ "--min-overlap",
208
+ dest="min_overlap",
209
+ type=int,
210
+ default=500,
211
+ help="延伸所需最小 overlap(默认 500 bp)",
212
+ )
213
+ parser.add_argument(
214
+ "--min-identity",
215
+ dest="min_identity",
216
+ type=float,
217
+ default=0.90,
218
+ help="延伸所需最小一致率(默认 0.90)",
219
+ )
220
+ parser.add_argument(
221
+ "--agreement-bp",
222
+ dest="agreement_bp",
223
+ type=int,
224
+ default=200,
225
+ help="多个邻居给出同一位置时允许的起点差(默认 200 bp)",
226
+ )
227
+ parser.set_defaults(handler=run_assemble)
228
+
229
+
230
+ def run_assemble(args: argparse.Namespace) -> int:
231
+ settings = assemble.AssembleSettings(
232
+ classes_dir=args.classes_dir,
233
+ paf=args.paf,
234
+ outdir=args.outdir,
235
+ min_overlap=args.min_overlap,
236
+ min_identity=args.min_identity,
237
+ agreement_bp=args.agreement_bp,
238
+ )
239
+ result = assemble.run(settings)
240
+ print(f"完成:{result['classes']} 个类,{result['contigs']} 条 contig;输出 {result['outdir']}")
241
+ return 0
242
+
243
+
244
+ def _add_locate(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
245
+ parser = subparsers.add_parser(
246
+ "locate",
247
+ parents=[GLOBAL_OPTIONS],
248
+ help="50 bp 断点锚点 + 外源查询 -> 丝带图 + 参考坐标还原",
249
+ description=(
250
+ "为每个类构造查询序列(断点旁 50 bp 锚点 + 全部外源),与类组装比对后逐条 contig 出图,"
251
+ "并把锚点/外源在 contig 上的落点还原成参考基因组坐标。"
252
+ ),
253
+ epilog=(
254
+ "例子:alnkit long locate \\\n"
255
+ " --regions 05_breakpoints/breakpoint_classes.tsv \\\n"
256
+ " --sites 05_breakpoints/breakpoint_sites.tsv \\\n"
257
+ " --reference-fasta reference.fa \\\n"
258
+ " --exog exog/h301_h.fa --exog exog/h301_l.fa \\\n"
259
+ " --assembly-dir 06_assembly --outdir 07_ribbon_positions"
260
+ ),
261
+ )
262
+ parser.add_argument("--regions", required=True, help="breakpoint_classes.tsv")
263
+ parser.add_argument("--sites", required=True, help="breakpoint_sites.tsv")
264
+ parser.add_argument(
265
+ "--reference-fasta",
266
+ dest="reference_fasta",
267
+ required=True,
268
+ help="参考基因组 fasta(需 .fai)",
269
+ )
270
+ parser.add_argument(
271
+ "--exog", action="append", default=[], required=True, help="外源 fasta,可重复"
272
+ )
273
+ parser.add_argument("--assembly-dir", dest="assembly_dir", required=True, help="06_assembly")
274
+ parser.add_argument("--outdir", required=True, help="输出目录")
275
+ parser.add_argument("--threads", type=int, default=12, help="minimap2 线程数(默认 12)")
276
+ parser.add_argument(
277
+ "--flank-bp", dest="flank_bp", type=int, default=50, help="断点锚点长度(默认 50 bp)"
278
+ )
279
+ parser.set_defaults(handler=run_locate)
280
+
281
+
282
+ def run_locate(args: argparse.Namespace) -> int:
283
+ settings = locate.LocateSettings(
284
+ regions=args.regions,
285
+ sites=args.sites,
286
+ reference_fasta=args.reference_fasta,
287
+ exog=tuple(args.exog),
288
+ assembly_dir=args.assembly_dir,
289
+ outdir=args.outdir,
290
+ threads=args.threads,
291
+ flank_bp=args.flank_bp,
292
+ )
293
+ result = locate.run(settings)
294
+ print(
295
+ f"完成:{result['classes']} 个类,{result['figures']} 张图,"
296
+ f"位置行 {result['position_rows']} 行;输出 {result['outdir']}"
297
+ )
298
+ return 0
299
+
300
+
301
+ def _add_graph(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
302
+ parser = subparsers.add_parser(
303
+ "graph",
304
+ parents=[GLOBAL_OPTIONS],
305
+ help="read-vs-read overlap 边与连通分量",
306
+ description=(
307
+ "读 all-vs-all PAF(minimap2 -x ava-pb -c),每对 read 只保留最好的一条 overlap,"
308
+ "再求全局连通分量。方案 A 的类别由断点共享 reads 决定,这里只用于观察 overlap 结构。"
309
+ ),
310
+ epilog=(
311
+ "例子:minimap2 -x ava-pb -c -t 8 related.fastq related.fastq > overlaps.paf\n"
312
+ " alnkit long graph --paf overlaps.paf \\\n"
313
+ " --classification 03_classify/read_classification.tsv --outdir 04_overlap"
314
+ ),
315
+ )
316
+ parser.add_argument("--paf", required=True, help="all-vs-all PAF(minimap2 -x ava-pb -c)")
317
+ parser.add_argument("--classification", required=True, help="read_classification.tsv")
318
+ parser.add_argument("--outdir", required=True, help="输出目录")
319
+ parser.add_argument(
320
+ "--min-overlap",
321
+ dest="min_overlap",
322
+ type=int,
323
+ default=300,
324
+ help="边所需最小 overlap(默认 300 bp)",
325
+ )
326
+ parser.add_argument(
327
+ "--min-identity",
328
+ dest="min_identity",
329
+ type=float,
330
+ default=0.85,
331
+ help="边所需最小一致率(默认 0.85)",
332
+ )
333
+ parser.set_defaults(handler=run_graph)
334
+
335
+
336
+ def run_graph(args: argparse.Namespace) -> int:
337
+ settings = graph.GraphSettings(
338
+ paf=args.paf,
339
+ classification=args.classification,
340
+ outdir=args.outdir,
341
+ min_overlap=args.min_overlap,
342
+ min_identity=args.min_identity,
343
+ )
344
+ result = graph.run(settings)
345
+ print(f"完成:overlap 边 {result['edges']} 条,连通分量 {result['components']} 个")
346
+ return 0