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.
Files changed (49) hide show
  1. alnkit/__init__.py +11 -0
  2. alnkit/__main__.py +10 -0
  3. alnkit/_docs.py +53 -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 +340 -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 +256 -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 +316 -0
  44. alnkit/shortread/scan.py +788 -0
  45. alnkit-0.2.0.dist-info/METADATA +163 -0
  46. alnkit-0.2.0.dist-info/RECORD +49 -0
  47. alnkit-0.2.0.dist-info/WHEEL +4 -0
  48. alnkit-0.2.0.dist-info/entry_points.txt +2 -0
  49. alnkit-0.2.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ # ---------------------------------------------------------------------------
3
+ # A4:read-vs-read overlap 边与连通分量
4
+ #
5
+ # 来源:upstream/scripts/overlap_graph.py(124 行)的移植。逻辑未改;
6
+ # 参数解析搬到 alnkit.cli.long、库层收 GraphSettings、main() 改为 run()。
7
+ # 三张产物与旧脚本逐字节一致。
8
+ #
9
+ # 输入:03_classify_reads/related_reads.fastq 的 all-vs-all PAF
10
+ # 03_classify_reads/read_classification.tsv
11
+ # 输出:04_overlap_graph/overlap_edges.tsv
12
+ # 04_overlap_graph/overlap_components.tsv
13
+ # 04_overlap_graph/overlap_summary.txt
14
+ # ---------------------------------------------------------------------------
15
+ from __future__ import annotations
16
+
17
+ import collections
18
+ import os
19
+ from dataclasses import dataclass
20
+
21
+ from ..core.paf import parse_paf
22
+ from ..core.tables import read_legacy_tsv as read_tsv
23
+ from ..core.tables import write_legacy_tsv as write_tsv
24
+
25
+
26
+ def load_read_categories(classification_path):
27
+ """读 read 类别,返回 {read_name: read_category}。"""
28
+ header, rows = read_tsv(classification_path)
29
+ index = {name: i for i, name in enumerate(header)}
30
+ return {row[index["read_name"]]: row[index["read_category"]] for row in rows}
31
+
32
+
33
+ def build_edges(blocks, min_overlap_bp, min_identity):
34
+ """每对 read 只保留 block_bp 最大的一条 overlap;返回边与邻接表。"""
35
+ best_by_pair = {}
36
+ for block in blocks:
37
+ if block["query"] == block["target"]:
38
+ continue
39
+ if block["block_bp"] < min_overlap_bp or block["identity"] < min_identity:
40
+ continue
41
+ pair = tuple(sorted((block["query"], block["target"])))
42
+ previous = best_by_pair.get(pair)
43
+ if previous is None or (block["block_bp"], block["identity"]) > (
44
+ previous["block_bp"],
45
+ previous["identity"],
46
+ ):
47
+ best_by_pair[pair] = block
48
+
49
+ edges = []
50
+ adjacency = collections.defaultdict(set)
51
+ for pair, block in sorted(best_by_pair.items()):
52
+ edges.append(block)
53
+ adjacency[block["query"]].add(block["target"])
54
+ adjacency[block["target"]].add(block["query"])
55
+ return edges, adjacency
56
+
57
+
58
+ def connected_components(read_names, adjacency):
59
+ """并查集求连通分量,按大小降序。"""
60
+ parent = {name: name for name in read_names}
61
+
62
+ def find(name):
63
+ while parent[name] != name:
64
+ parent[name] = parent[parent[name]]
65
+ name = parent[name]
66
+ return name
67
+
68
+ def union(left, right):
69
+ left_root, right_root = find(left), find(right)
70
+ if left_root != right_root:
71
+ parent[left_root] = right_root
72
+
73
+ for read_name, neighbors in adjacency.items():
74
+ for neighbor in neighbors:
75
+ union(read_name, neighbor)
76
+
77
+ groups = collections.defaultdict(set)
78
+ for read_name in read_names:
79
+ groups[find(read_name)].add(read_name)
80
+ return sorted(groups.values(), key=lambda members: (-len(members), sorted(members)[0]))
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class GraphSettings:
85
+ """``alnkit long graph`` 的全部入参(与旧脚本参数一一对应)。"""
86
+
87
+ paf: str = ""
88
+ classification: str = ""
89
+ outdir: str = "."
90
+ min_overlap: int = 300
91
+ min_identity: float = 0.85
92
+
93
+
94
+ def run(settings: GraphSettings) -> dict[str, object]:
95
+ os.makedirs(settings.outdir, exist_ok=True)
96
+
97
+ categories = load_read_categories(settings.classification)
98
+ blocks = parse_paf(settings.paf)
99
+ edges, adjacency = build_edges(blocks, settings.min_overlap, settings.min_identity)
100
+ components = connected_components(set(categories), adjacency)
101
+
102
+ write_tsv(
103
+ os.path.join(settings.outdir, "overlap_edges.tsv"),
104
+ [
105
+ "read_a",
106
+ "read_b",
107
+ "block_bp",
108
+ "identity",
109
+ "strand",
110
+ "read_a_start",
111
+ "read_a_end",
112
+ "read_b_start",
113
+ "read_b_end",
114
+ ],
115
+ [
116
+ [
117
+ edge["query"],
118
+ edge["target"],
119
+ edge["block_bp"],
120
+ "{0:.4f}".format(edge["identity"]),
121
+ edge["strand"],
122
+ edge["query_start"],
123
+ edge["query_end"],
124
+ edge["target_start"],
125
+ edge["target_end"],
126
+ ]
127
+ for edge in edges
128
+ ],
129
+ [
130
+ "每对 read 只保留 block_bp 最大的一条 overlap;坐标 0-based 半开。",
131
+ "阈值:block_bp >= --min-overlap,identity >= --min-identity。",
132
+ ],
133
+ )
134
+
135
+ component_rows = []
136
+ for index, members in enumerate(components, 1):
137
+ component_rows.append(
138
+ [
139
+ "C{0:02d}".format(index),
140
+ len(members),
141
+ ",".join(sorted({categories.get(name, "-") for name in members})),
142
+ sum(1 for name in members if categories.get(name) == "vector_region"),
143
+ ";".join(sorted(members)),
144
+ ]
145
+ )
146
+ write_tsv(
147
+ os.path.join(settings.outdir, "overlap_components.tsv"),
148
+ [
149
+ "component_id",
150
+ "n_reads",
151
+ "categories",
152
+ "n_vector_reads",
153
+ "read_names",
154
+ ],
155
+ component_rows,
156
+ [
157
+ "全局连通分量,只用于观察 overlap 结构;方案 A 的类别由断点共享 reads 决定。",
158
+ ],
159
+ )
160
+
161
+ summary = [
162
+ "overlap 边: {0}".format(len(edges)),
163
+ "连通分量: {0}".format(len(components)),
164
+ "最大分量 reads: {0}".format(len(components[0]) if components else 0),
165
+ ]
166
+ with open(os.path.join(settings.outdir, "overlap_summary.txt"), "w") as output:
167
+ output.write("\n".join(summary) + "\n")
168
+ print("\n".join(summary))
169
+
170
+ return {
171
+ "outdir": settings.outdir,
172
+ "edges": len(edges),
173
+ "components": len(components),
174
+ }