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
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# A5:按 read 轴上“紧邻片段”定义断点,并按共享 reads 合并成类
|
|
4
|
+
#
|
|
5
|
+
# 来源:upstream/scripts/breakpoints.py(711 行)的忠实移植。逻辑未改;
|
|
6
|
+
# 相对原脚本只有三处包装:参数解析搬到 alnkit.cli.long、库层收 BreakpointSettings、
|
|
7
|
+
# main() 改为 run()。输出表 / classes 子集 FASTQ / graph_summary.txt 与旧脚本逐字节一致。
|
|
8
|
+
# 已知(保留不改):class_read_pools.tsv 的 reads_fastq 列是绝对路径,换 outdir 就会变;
|
|
9
|
+
# 下游要可移植的路径,得等这几个三代表统一改契约(单独立项)。
|
|
10
|
+
#
|
|
11
|
+
# 与旧 200 kb 区域分类的区别:
|
|
12
|
+
# * 末端类型看 read 轴上紧邻的片段:
|
|
13
|
+
# - 基因组–外源 -> vector 断点(S##)
|
|
14
|
+
# - 基因组–基因组 -> genome 断点(G##a/G##b)
|
|
15
|
+
# - 其他/无 -> 不进入断点图
|
|
16
|
+
# * 两个断点之间 >= --min-shared 条直接共享 reads -> 合并为同一类(方案 A)。
|
|
17
|
+
# * 3000000(genome_right)会被识别为 G01a:
|
|
18
|
+
# 它紧邻的是 genome B,不是外源;B 的另一端才是 S04。
|
|
19
|
+
#
|
|
20
|
+
# 输入:
|
|
21
|
+
# 03_classify_reads/alignment_segments.tsv 目标 read 的比对段(kept=1)
|
|
22
|
+
# 03_classify_reads/read_classification.tsv read 类别与长度
|
|
23
|
+
# 04_overlap_graph/overlap_edges.tsv read-read overlap 边
|
|
24
|
+
# 03_classify_reads/related_reads.fastq 输出每类 reads 子集
|
|
25
|
+
# 01_merge_ref/merged_reference.fa.fai 取序列长度(50bp 窗口截断)
|
|
26
|
+
# 输出(到 05_breakpoints/):
|
|
27
|
+
# breakpoint_sites.tsv vector/genome 断点节点
|
|
28
|
+
# breakpoint_classes.tsv 方案 A 的类(供 07 丝带图使用)
|
|
29
|
+
# breakpoint_shared_reads.tsv 断点对的共享 reads / unique overlap 支持
|
|
30
|
+
# segment_ends.tsv 每个 genome 片段末端的紧邻信息(含非朝向端)
|
|
31
|
+
# class_read_pools.tsv 每类 read 池(供 06 组装)
|
|
32
|
+
# classes/<class>/*.fastq 每类 reads / junction / vector
|
|
33
|
+
# graph_summary.txt 控制台摘要
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import collections
|
|
38
|
+
import os
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
|
|
41
|
+
import pysam
|
|
42
|
+
|
|
43
|
+
from ..core.bam_segments import load_fai_lengths
|
|
44
|
+
from ..core.tables import read_legacy_tsv as read_tsv
|
|
45
|
+
from ..core.tables import write_legacy_tsv as write_tsv
|
|
46
|
+
|
|
47
|
+
### 读取输入 ###
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_segments_by_read(segments_path):
|
|
51
|
+
"""读 alignment_segments.tsv,返回 {read_name: [segment, ...]}(kept=1)。
|
|
52
|
+
|
|
53
|
+
输入示例: load_segments_by_read("alignment_segments.tsv")
|
|
54
|
+
输出示例: {"r1": [{"role": "genome", "chrom": "chr1", ...}, ...]}
|
|
55
|
+
"""
|
|
56
|
+
header, rows = read_tsv(segments_path)
|
|
57
|
+
index = {name: i for i, name in enumerate(header)}
|
|
58
|
+
segments_by_read = collections.defaultdict(list)
|
|
59
|
+
for row in rows:
|
|
60
|
+
if row[index["kept"]] != "1":
|
|
61
|
+
continue
|
|
62
|
+
role = row[index["segment_role"]]
|
|
63
|
+
if role not in ("genome", "exog"):
|
|
64
|
+
continue
|
|
65
|
+
segments_by_read[row[index["read_name"]]].append(
|
|
66
|
+
{
|
|
67
|
+
"read_name": row[index["read_name"]],
|
|
68
|
+
"role": role,
|
|
69
|
+
"chrom": row[index["chrom"]],
|
|
70
|
+
"ref_start": int(row[index["ref_start"]]),
|
|
71
|
+
"ref_end": int(row[index["ref_end"]]),
|
|
72
|
+
"read_start": int(row[index["read_start"]]),
|
|
73
|
+
"read_end": int(row[index["read_end"]]),
|
|
74
|
+
"strand": row[index["strand"]],
|
|
75
|
+
"mapq": int(row[index["mapq"]]),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
for segments in segments_by_read.values():
|
|
79
|
+
segments.sort(key=lambda segment: (segment["read_start"], segment["read_end"]))
|
|
80
|
+
return segments_by_read
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_classification(classification_path):
|
|
84
|
+
"""读 read 分类表,返回 {read_name: {"length", "category"}}。
|
|
85
|
+
|
|
86
|
+
输入示例: load_classification("read_classification.tsv")
|
|
87
|
+
输出示例: {"r1": {"length": 19000, "category": "vector_region"}}
|
|
88
|
+
"""
|
|
89
|
+
header, rows = read_tsv(classification_path)
|
|
90
|
+
index = {name: i for i, name in enumerate(header)}
|
|
91
|
+
classification = {}
|
|
92
|
+
for row in rows:
|
|
93
|
+
classification[row[index["read_name"]]] = {
|
|
94
|
+
"length": int(row[index["read_length"]]),
|
|
95
|
+
"category": row[index["read_category"]],
|
|
96
|
+
}
|
|
97
|
+
return classification
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_overlap_edges(edges_path):
|
|
101
|
+
"""读 overlap_edges.tsv,返回无向邻接表。
|
|
102
|
+
|
|
103
|
+
输入示例: load_overlap_edges("overlap_edges.tsv")
|
|
104
|
+
输出示例: {"r1": {"r2"}, "r2": {"r1"}}
|
|
105
|
+
"""
|
|
106
|
+
header, rows = read_tsv(edges_path)
|
|
107
|
+
index = {name: i for i, name in enumerate(header)}
|
|
108
|
+
adjacency = collections.defaultdict(set)
|
|
109
|
+
for row in rows:
|
|
110
|
+
read_a = row[index["read_a"]]
|
|
111
|
+
read_b = row[index["read_b"]]
|
|
112
|
+
adjacency[read_a].add(read_b)
|
|
113
|
+
adjacency[read_b].add(read_a)
|
|
114
|
+
return adjacency
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
### 片段末端与 read 轴紧邻关系 ###
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def segment_end_info(segment, side):
|
|
121
|
+
"""给 genome 片段的左/右端算出参考边界与 genome_side。
|
|
122
|
+
|
|
123
|
+
输入示例: segment=strand "-" 的 3100000-3101000, side="left"
|
|
124
|
+
输出示例: {"side": "left", "boundary": 3101000,
|
|
125
|
+
"genome_side": "genome_left"}
|
|
126
|
+
说明:
|
|
127
|
+
* 左端(read 轴左侧):
|
|
128
|
+
strand + -> ref_start,genome_right
|
|
129
|
+
strand - -> ref_end, genome_left
|
|
130
|
+
* 右端(read 轴右侧):
|
|
131
|
+
strand + -> ref_end, genome_left
|
|
132
|
+
strand - -> ref_start,genome_right
|
|
133
|
+
"""
|
|
134
|
+
if side == "left":
|
|
135
|
+
if segment["strand"] == "+":
|
|
136
|
+
return {"side": side, "boundary": segment["ref_start"], "genome_side": "genome_right"}
|
|
137
|
+
return {"side": side, "boundary": segment["ref_end"], "genome_side": "genome_left"}
|
|
138
|
+
if segment["strand"] == "+":
|
|
139
|
+
return {"side": side, "boundary": segment["ref_end"], "genome_side": "genome_left"}
|
|
140
|
+
return {"side": side, "boundary": segment["ref_start"], "genome_side": "genome_right"}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def find_neighbor(segment, segments, side):
|
|
144
|
+
"""找 genome 片段在 read 轴左/右的紧邻片段(不要求无间隔)。
|
|
145
|
+
|
|
146
|
+
输入示例: segment=A(read 0-5334),side="right"
|
|
147
|
+
输出示例: B(read 5331-7550,genome)
|
|
148
|
+
说明:左邻 = 起点更小且终点更小的片段中 read_end 最大者;
|
|
149
|
+
右邻 = 起点更大且终点更大的片段中 read_start 最小者。
|
|
150
|
+
这样会跳过完全包含在当前片段内部的小片段。
|
|
151
|
+
"""
|
|
152
|
+
if side == "left":
|
|
153
|
+
candidates = [
|
|
154
|
+
other
|
|
155
|
+
for other in segments
|
|
156
|
+
if other is not segment
|
|
157
|
+
and other["read_start"] < segment["read_start"]
|
|
158
|
+
and other["read_end"] < segment["read_end"]
|
|
159
|
+
]
|
|
160
|
+
return max(candidates, key=lambda other: other["read_end"], default=None)
|
|
161
|
+
candidates = [
|
|
162
|
+
other
|
|
163
|
+
for other in segments
|
|
164
|
+
if other is not segment
|
|
165
|
+
and other["read_start"] > segment["read_start"]
|
|
166
|
+
and other["read_end"] > segment["read_end"]
|
|
167
|
+
]
|
|
168
|
+
return min(candidates, key=lambda other: other["read_start"], default=None)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def neighbor_gap(segment, neighbor, side):
|
|
172
|
+
"""算 genome 片段与紧邻片段的 read 轴间隔(重叠记 0)。
|
|
173
|
+
|
|
174
|
+
输入示例: segment=B(5331-7550),neighbor=exog(10876-12229),side="right"
|
|
175
|
+
输出示例: 3326
|
|
176
|
+
"""
|
|
177
|
+
if neighbor is None:
|
|
178
|
+
return "-"
|
|
179
|
+
if side == "left":
|
|
180
|
+
return max(0, segment["read_start"] - neighbor["read_end"])
|
|
181
|
+
return max(0, neighbor["read_start"] - segment["read_end"])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def window_50bp(genome_side, boundary, chrom_length):
|
|
185
|
+
"""按 genome_side 取边界旁 50 bp 窗口(1-based 展示)。
|
|
186
|
+
|
|
187
|
+
输入示例: window_50bp("genome_left", 3101000, 100000000)
|
|
188
|
+
输出示例: "chr1:...-..." 不拼 chrom;返回 (start, end)
|
|
189
|
+
"""
|
|
190
|
+
if genome_side == "genome_left":
|
|
191
|
+
start = max(0, boundary - 50)
|
|
192
|
+
end = min(chrom_length, boundary)
|
|
193
|
+
else:
|
|
194
|
+
start = max(0, boundary)
|
|
195
|
+
end = min(chrom_length, boundary + 50)
|
|
196
|
+
return start, end
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_read_axis_events(segments_by_read, chrom_lengths):
|
|
200
|
+
"""扫描每条 read 的片段,生成断点模板、segment_ends 行。
|
|
201
|
+
|
|
202
|
+
输入示例: segments_by_read={"r1": [genomeA, genomeB, exog1]}
|
|
203
|
+
输出示例: (templates, genome_pair_events, segment_end_rows)
|
|
204
|
+
templates: [{"junction_type": "genome"/"vector", "chrom": ..., "genome_side": ...}]
|
|
205
|
+
genome_pair_events: [(read_name, template_right, template_left), ...]
|
|
206
|
+
segment_end_rows: 每个 genome 片段左右端一行
|
|
207
|
+
"""
|
|
208
|
+
templates = []
|
|
209
|
+
genome_pair_events = []
|
|
210
|
+
segment_end_rows = []
|
|
211
|
+
|
|
212
|
+
for read_name, segments in segments_by_read.items():
|
|
213
|
+
for segment in segments:
|
|
214
|
+
if segment["role"] != "genome":
|
|
215
|
+
continue
|
|
216
|
+
left_neighbor = find_neighbor(segment, segments, "left")
|
|
217
|
+
right_neighbor = find_neighbor(segment, segments, "right")
|
|
218
|
+
|
|
219
|
+
# genome 片段的左端
|
|
220
|
+
left_info = segment_end_info(segment, "left")
|
|
221
|
+
left_adjacent_coordinate = "-"
|
|
222
|
+
if left_neighbor is not None and left_neighbor["role"] == "genome":
|
|
223
|
+
left_adjacent_coordinate = segment_end_info(left_neighbor, "right")["boundary"]
|
|
224
|
+
left_junction_type = (
|
|
225
|
+
"genome"
|
|
226
|
+
if left_neighbor is not None and left_neighbor["role"] == "genome"
|
|
227
|
+
else "vector"
|
|
228
|
+
if left_neighbor is not None and left_neighbor["role"] == "exog"
|
|
229
|
+
else "none"
|
|
230
|
+
)
|
|
231
|
+
left_window = window_50bp(
|
|
232
|
+
left_info["genome_side"],
|
|
233
|
+
left_info["boundary"],
|
|
234
|
+
chrom_lengths.get(segment["chrom"], left_info["boundary"] + 50),
|
|
235
|
+
)
|
|
236
|
+
segment_end_rows.append(
|
|
237
|
+
{
|
|
238
|
+
"read_name": read_name,
|
|
239
|
+
"chrom": segment["chrom"],
|
|
240
|
+
"segment_start": segment["ref_start"],
|
|
241
|
+
"segment_end": segment["ref_end"],
|
|
242
|
+
"strand": segment["strand"],
|
|
243
|
+
"end_side": "left",
|
|
244
|
+
"end_coordinate": left_info["boundary"],
|
|
245
|
+
"genome_side": left_info["genome_side"],
|
|
246
|
+
"adjacent_type": left_neighbor["role"] if left_neighbor else "none",
|
|
247
|
+
"adjacent_name": left_neighbor["chrom"] if left_neighbor else "-",
|
|
248
|
+
"adjacent_coordinate": left_adjacent_coordinate,
|
|
249
|
+
"junction_type": left_junction_type,
|
|
250
|
+
"gap_bp": neighbor_gap(segment, left_neighbor, "left"),
|
|
251
|
+
"window_50bp": "{0}:{1}-{2}".format(
|
|
252
|
+
segment["chrom"], left_window[0] + 1, left_window[1]
|
|
253
|
+
),
|
|
254
|
+
"junction_id": "-",
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
if left_neighbor is not None and left_neighbor["role"] == "exog":
|
|
258
|
+
templates.append(
|
|
259
|
+
{
|
|
260
|
+
"read_name": read_name,
|
|
261
|
+
"junction_type": "vector",
|
|
262
|
+
"chrom": segment["chrom"],
|
|
263
|
+
"genome_side": left_info["genome_side"],
|
|
264
|
+
"boundary": left_info["boundary"],
|
|
265
|
+
"exog_name": left_neighbor["chrom"],
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# genome 片段的右端
|
|
270
|
+
right_info = segment_end_info(segment, "right")
|
|
271
|
+
right_adjacent_coordinate = "-"
|
|
272
|
+
if right_neighbor is not None and right_neighbor["role"] == "genome":
|
|
273
|
+
right_adjacent_coordinate = segment_end_info(right_neighbor, "left")["boundary"]
|
|
274
|
+
right_junction_type = (
|
|
275
|
+
"genome"
|
|
276
|
+
if right_neighbor is not None and right_neighbor["role"] == "genome"
|
|
277
|
+
else "vector"
|
|
278
|
+
if right_neighbor is not None and right_neighbor["role"] == "exog"
|
|
279
|
+
else "none"
|
|
280
|
+
)
|
|
281
|
+
right_window = window_50bp(
|
|
282
|
+
right_info["genome_side"],
|
|
283
|
+
right_info["boundary"],
|
|
284
|
+
chrom_lengths.get(segment["chrom"], right_info["boundary"] + 50),
|
|
285
|
+
)
|
|
286
|
+
segment_end_rows.append(
|
|
287
|
+
{
|
|
288
|
+
"read_name": read_name,
|
|
289
|
+
"chrom": segment["chrom"],
|
|
290
|
+
"segment_start": segment["ref_start"],
|
|
291
|
+
"segment_end": segment["ref_end"],
|
|
292
|
+
"strand": segment["strand"],
|
|
293
|
+
"end_side": "right",
|
|
294
|
+
"end_coordinate": right_info["boundary"],
|
|
295
|
+
"genome_side": right_info["genome_side"],
|
|
296
|
+
"adjacent_type": right_neighbor["role"] if right_neighbor else "none",
|
|
297
|
+
"adjacent_name": right_neighbor["chrom"] if right_neighbor else "-",
|
|
298
|
+
"adjacent_coordinate": right_adjacent_coordinate,
|
|
299
|
+
"junction_type": right_junction_type,
|
|
300
|
+
"gap_bp": neighbor_gap(segment, right_neighbor, "right"),
|
|
301
|
+
"window_50bp": "{0}:{1}-{2}".format(
|
|
302
|
+
segment["chrom"], right_window[0] + 1, right_window[1]
|
|
303
|
+
),
|
|
304
|
+
"junction_id": "-",
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
if right_neighbor is not None and right_neighbor["role"] == "exog":
|
|
308
|
+
templates.append(
|
|
309
|
+
{
|
|
310
|
+
"read_name": read_name,
|
|
311
|
+
"junction_type": "vector",
|
|
312
|
+
"chrom": segment["chrom"],
|
|
313
|
+
"genome_side": right_info["genome_side"],
|
|
314
|
+
"boundary": right_info["boundary"],
|
|
315
|
+
"exog_name": right_neighbor["chrom"],
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
elif right_neighbor is not None and right_neighbor["role"] == "genome":
|
|
319
|
+
right_template = {
|
|
320
|
+
"read_name": read_name,
|
|
321
|
+
"junction_type": "genome",
|
|
322
|
+
"chrom": segment["chrom"],
|
|
323
|
+
"genome_side": right_info["genome_side"],
|
|
324
|
+
"boundary": right_info["boundary"],
|
|
325
|
+
"exog_name": "-",
|
|
326
|
+
}
|
|
327
|
+
left_template = {
|
|
328
|
+
"read_name": read_name,
|
|
329
|
+
"junction_type": "genome",
|
|
330
|
+
"chrom": right_neighbor["chrom"],
|
|
331
|
+
"genome_side": segment_end_info(right_neighbor, "left")["genome_side"],
|
|
332
|
+
"boundary": segment_end_info(right_neighbor, "left")["boundary"],
|
|
333
|
+
"exog_name": "-",
|
|
334
|
+
}
|
|
335
|
+
templates.extend([right_template, left_template])
|
|
336
|
+
genome_pair_events.append((read_name, right_template, left_template))
|
|
337
|
+
|
|
338
|
+
return templates, genome_pair_events, segment_end_rows
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
### 断点聚类与合并 ###
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def make_cluster(templates):
|
|
345
|
+
"""把一簇模板整理成 site 记录。
|
|
346
|
+
|
|
347
|
+
输入示例: templates=[同一 side/边界的多个 read 模板]
|
|
348
|
+
输出示例: {"junction_type": "vector", "chrom": ..., "genome_side": ...,
|
|
349
|
+
"boundary_min": ..., "boundary_max": ..., "boundary_median": ...,
|
|
350
|
+
"read_names": {...}, "exog_names": {...}, "templates": [...]}
|
|
351
|
+
"""
|
|
352
|
+
boundaries = sorted(template["boundary"] for template in templates)
|
|
353
|
+
return {
|
|
354
|
+
"junction_type": templates[0]["junction_type"],
|
|
355
|
+
"chrom": templates[0]["chrom"],
|
|
356
|
+
"genome_side": templates[0]["genome_side"],
|
|
357
|
+
"boundary_min": boundaries[0],
|
|
358
|
+
"boundary_max": boundaries[-1],
|
|
359
|
+
"boundary_median": boundaries[len(boundaries) // 2],
|
|
360
|
+
"read_names": {template["read_name"] for template in templates},
|
|
361
|
+
"exog_names": {
|
|
362
|
+
template["exog_name"]
|
|
363
|
+
for template in templates
|
|
364
|
+
if template.get("exog_name") not in (None, "-")
|
|
365
|
+
},
|
|
366
|
+
"templates": list(templates),
|
|
367
|
+
"site_id": None,
|
|
368
|
+
"junction_pair": "-",
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def cluster_templates(templates, gap_bp):
|
|
373
|
+
"""按 (junction_type, chrom, genome_side) 聚断点;边界差 <= gap 为同一 site。
|
|
374
|
+
|
|
375
|
+
输入示例: cluster_templates(templates, 20)
|
|
376
|
+
输出示例: [cluster, ...](按 chrom/boundary 排序)
|
|
377
|
+
"""
|
|
378
|
+
grouped = collections.defaultdict(list)
|
|
379
|
+
for template in templates:
|
|
380
|
+
grouped[(template["junction_type"], template["chrom"], template["genome_side"])].append(
|
|
381
|
+
template
|
|
382
|
+
)
|
|
383
|
+
clusters = []
|
|
384
|
+
for key in sorted(grouped):
|
|
385
|
+
group = sorted(grouped[key], key=lambda template: template["boundary"])
|
|
386
|
+
current = []
|
|
387
|
+
for template in group:
|
|
388
|
+
if current and template["boundary"] - current[-1]["boundary"] > gap_bp:
|
|
389
|
+
clusters.append(make_cluster(current))
|
|
390
|
+
current = []
|
|
391
|
+
current.append(template)
|
|
392
|
+
if current:
|
|
393
|
+
clusters.append(make_cluster(current))
|
|
394
|
+
clusters.sort(
|
|
395
|
+
key=lambda cluster: (cluster["chrom"], cluster["boundary_median"], cluster["junction_type"])
|
|
396
|
+
)
|
|
397
|
+
return clusters
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def assign_vector_site_ids(clusters):
|
|
401
|
+
"""按染色体+边界给 vector 断点分配本地 S01/S02… 编号。
|
|
402
|
+
|
|
403
|
+
输入示例: clusters=[vector cluster...]
|
|
404
|
+
输出示例: 修改 cluster["site_id"](S01、S02…)
|
|
405
|
+
说明:独立项目不复用旧流程的 S## 编号,全部按本数据坐标重新编号。
|
|
406
|
+
"""
|
|
407
|
+
vector_clusters = [cluster for cluster in clusters if cluster["junction_type"] == "vector"]
|
|
408
|
+
vector_clusters.sort(key=lambda cluster: (cluster["chrom"], cluster["boundary_median"]))
|
|
409
|
+
for index, cluster in enumerate(vector_clusters, 1):
|
|
410
|
+
cluster["site_id"] = "S{0:02d}".format(index)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def assign_genome_site_ids(clusters, genome_pair_events, cluster_by_template):
|
|
414
|
+
"""按 genome–genome 配对给 genome cluster 编号 G01a/G01b。
|
|
415
|
+
|
|
416
|
+
输入示例: genome_pair_events=[(read, template_right, template_left), ...]
|
|
417
|
+
输出示例: 修改 cluster["site_id"];返回 genome pair 列表
|
|
418
|
+
"""
|
|
419
|
+
cluster_index = {id(cluster): index for index, cluster in enumerate(clusters)}
|
|
420
|
+
pair_counts = collections.Counter()
|
|
421
|
+
for _, right_template, left_template in genome_pair_events:
|
|
422
|
+
right_cluster = cluster_by_template[id(right_template)]
|
|
423
|
+
left_cluster = cluster_by_template[id(left_template)]
|
|
424
|
+
pair_counts[
|
|
425
|
+
tuple(sorted((cluster_index[id(right_cluster)], cluster_index[id(left_cluster)])))
|
|
426
|
+
] += 1
|
|
427
|
+
|
|
428
|
+
pairs = []
|
|
429
|
+
for pair_key in pair_counts:
|
|
430
|
+
pair_clusters = [clusters[index] for index in pair_key]
|
|
431
|
+
pair_clusters.sort(key=lambda cluster: cluster["boundary_median"])
|
|
432
|
+
pairs.append((pair_clusters, pair_counts[pair_key]))
|
|
433
|
+
pairs.sort(key=lambda item: (item[0][0]["chrom"], item[0][0]["boundary_median"]))
|
|
434
|
+
|
|
435
|
+
for index, (pair_clusters, _) in enumerate(pairs, 1):
|
|
436
|
+
pair_name = "G{0:02d}".format(index)
|
|
437
|
+
for suffix, cluster in zip(("a", "b"), pair_clusters):
|
|
438
|
+
if cluster["site_id"] is None:
|
|
439
|
+
cluster["site_id"] = pair_name + suffix
|
|
440
|
+
cluster["junction_pair"] = pair_name
|
|
441
|
+
pairs[index - 1] = (pair_clusters, pair_name)
|
|
442
|
+
return pairs
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def assign_region_ids(clusters, shared_counts, min_shared):
|
|
446
|
+
"""按共享 reads >= min_shared 合并断点,返回 classes 与 site->class 映射。
|
|
447
|
+
|
|
448
|
+
输入示例: shared_counts={(S05,G01a): 5, ...}, min_shared=2
|
|
449
|
+
输出示例: classes=[{"class_id": "C01", "sites": [...]}, ...]
|
|
450
|
+
"""
|
|
451
|
+
site_ids = [cluster["site_id"] for cluster in clusters]
|
|
452
|
+
parent = {site_id: site_id for site_id in site_ids}
|
|
453
|
+
|
|
454
|
+
def find(site_id):
|
|
455
|
+
while parent[site_id] != site_id:
|
|
456
|
+
parent[site_id] = parent[parent[site_id]]
|
|
457
|
+
site_id = parent[site_id]
|
|
458
|
+
return site_id
|
|
459
|
+
|
|
460
|
+
def union(site_a, site_b):
|
|
461
|
+
root_a, root_b = find(site_a), find(site_b)
|
|
462
|
+
if root_a != root_b:
|
|
463
|
+
parent[root_a] = root_b
|
|
464
|
+
|
|
465
|
+
for (site_a, site_b), count in shared_counts.items():
|
|
466
|
+
if count >= min_shared:
|
|
467
|
+
union(site_a, site_b)
|
|
468
|
+
|
|
469
|
+
groups = collections.defaultdict(list)
|
|
470
|
+
for cluster in clusters:
|
|
471
|
+
groups[find(cluster["site_id"])].append(cluster)
|
|
472
|
+
|
|
473
|
+
class_sites = list(groups.values())
|
|
474
|
+
class_sites.sort(
|
|
475
|
+
key=lambda sites: (
|
|
476
|
+
min(site["chrom"] for site in sites),
|
|
477
|
+
min(site["boundary_min"] for site in sites),
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
classes = []
|
|
481
|
+
site_to_class = {}
|
|
482
|
+
for index, sites in enumerate(class_sites, 1):
|
|
483
|
+
class_id = "C{0:02d}".format(index)
|
|
484
|
+
site_ids_sorted = sorted(site["site_id"] for site in sites)
|
|
485
|
+
for site in sites:
|
|
486
|
+
site_to_class[site["site_id"]] = class_id
|
|
487
|
+
classes.append(
|
|
488
|
+
{
|
|
489
|
+
"class_id": class_id,
|
|
490
|
+
"sites": sites,
|
|
491
|
+
"site_ids": site_ids_sorted,
|
|
492
|
+
"junction_read_names": set().union(*(site["read_names"] for site in sites)),
|
|
493
|
+
"exog_names": set().union(*(site["exog_names"] for site in sites)),
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
return classes, site_to_class
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
### 输出 ###
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def fill_segment_end_junction_ids(segment_end_rows, clusters):
|
|
503
|
+
"""给 segment_ends 行填 junction_id(按 type/chrom/side/boundary 匹配 site)。"""
|
|
504
|
+
for row in segment_end_rows:
|
|
505
|
+
candidates = [
|
|
506
|
+
cluster
|
|
507
|
+
for cluster in clusters
|
|
508
|
+
if cluster["junction_type"] == row["junction_type"]
|
|
509
|
+
and cluster["chrom"] == row["chrom"]
|
|
510
|
+
and cluster["genome_side"] == row["genome_side"]
|
|
511
|
+
and abs(cluster["boundary_median"] - row["end_coordinate"]) <= 20
|
|
512
|
+
]
|
|
513
|
+
if candidates:
|
|
514
|
+
row["junction_id"] = min(
|
|
515
|
+
candidates,
|
|
516
|
+
key=lambda cluster: abs(cluster["boundary_median"] - row["end_coordinate"]),
|
|
517
|
+
)["site_id"]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def write_fastq_subsets(reads_path, read_names_by_output_path):
|
|
521
|
+
"""一次扫描 FASTQ,写出所有类别子集。"""
|
|
522
|
+
handles = {}
|
|
523
|
+
try:
|
|
524
|
+
for output_path in read_names_by_output_path:
|
|
525
|
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
526
|
+
handles[output_path] = open(output_path, "w")
|
|
527
|
+
with pysam.FastxFile(reads_path) as fastq:
|
|
528
|
+
for record in fastq:
|
|
529
|
+
for output_path, selected_names in read_names_by_output_path.items():
|
|
530
|
+
if record.name in selected_names:
|
|
531
|
+
handles[output_path].write(
|
|
532
|
+
"@{0}\n{1}\n+\n{2}\n".format(
|
|
533
|
+
record.name, record.sequence, record.quality
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
finally:
|
|
537
|
+
for handle in handles.values():
|
|
538
|
+
handle.close()
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
### 主流程 ###
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@dataclass(slots=True)
|
|
545
|
+
class BreakpointSettings:
|
|
546
|
+
"""``alnkit long breakpoints`` 的全部入参(与旧脚本参数一一对应)。"""
|
|
547
|
+
|
|
548
|
+
segments: str = ""
|
|
549
|
+
classification: str = ""
|
|
550
|
+
overlaps: str = ""
|
|
551
|
+
reads: str = ""
|
|
552
|
+
merged_fai: str = ""
|
|
553
|
+
outdir: str = "."
|
|
554
|
+
site_gap: int = 20
|
|
555
|
+
min_shared: int = 2
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def run(settings: BreakpointSettings) -> dict[str, object]:
|
|
559
|
+
os.makedirs(settings.outdir, exist_ok=True)
|
|
560
|
+
chrom_lengths = load_fai_lengths(settings.merged_fai)
|
|
561
|
+
segments_by_read = load_segments_by_read(settings.segments)
|
|
562
|
+
classification = load_classification(settings.classification)
|
|
563
|
+
overlap_adjacency = load_overlap_edges(settings.overlaps)
|
|
564
|
+
|
|
565
|
+
### 1. read 轴邻接 -> 模板 / segment ends ###
|
|
566
|
+
|
|
567
|
+
templates, genome_pair_events, segment_end_rows = build_read_axis_events(
|
|
568
|
+
segments_by_read, chrom_lengths
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
### 2. 断点聚类与编号 ###
|
|
572
|
+
|
|
573
|
+
clusters = cluster_templates(templates, settings.site_gap)
|
|
574
|
+
assign_vector_site_ids(clusters)
|
|
575
|
+
cluster_by_template = {}
|
|
576
|
+
for cluster in clusters:
|
|
577
|
+
for template in cluster["templates"]:
|
|
578
|
+
cluster_by_template[id(template)] = cluster
|
|
579
|
+
genome_pairs = assign_genome_site_ids(clusters, genome_pair_events, cluster_by_template)
|
|
580
|
+
|
|
581
|
+
# 没有配对的 genome cluster(极端情况):给一个独立 G 编号
|
|
582
|
+
next_genome_index = len(genome_pairs) + 1
|
|
583
|
+
for cluster in clusters:
|
|
584
|
+
if cluster["junction_type"] == "genome" and cluster["site_id"] is None:
|
|
585
|
+
cluster["site_id"] = "G{0:02d}u".format(next_genome_index)
|
|
586
|
+
cluster["junction_pair"] = "G{0:02d}".format(next_genome_index)
|
|
587
|
+
next_genome_index += 1
|
|
588
|
+
|
|
589
|
+
### 3. 共享 reads 与方案 A 类别 ###
|
|
590
|
+
|
|
591
|
+
supported_sites_by_read = collections.defaultdict(set)
|
|
592
|
+
for cluster in clusters:
|
|
593
|
+
for template in cluster["templates"]:
|
|
594
|
+
supported_sites_by_read[template["read_name"]].add(cluster["site_id"])
|
|
595
|
+
|
|
596
|
+
shared_counts = collections.Counter()
|
|
597
|
+
for read_name, site_ids in supported_sites_by_read.items():
|
|
598
|
+
sorted_sites = sorted(site_ids)
|
|
599
|
+
for left_index in range(len(sorted_sites)):
|
|
600
|
+
for right_index in range(left_index + 1, len(sorted_sites)):
|
|
601
|
+
shared_counts[(sorted_sites[left_index], sorted_sites[right_index])] += 1
|
|
602
|
+
|
|
603
|
+
support_by_site = {cluster["site_id"]: set(cluster["read_names"]) for cluster in clusters}
|
|
604
|
+
shared_rows = []
|
|
605
|
+
for (site_a, site_b), shared in sorted(
|
|
606
|
+
shared_counts.items(), key=lambda item: (-item[1], item[0])
|
|
607
|
+
):
|
|
608
|
+
unique_a = support_by_site[site_a] - support_by_site[site_b]
|
|
609
|
+
unique_b = support_by_site[site_b] - support_by_site[site_a]
|
|
610
|
+
smaller, larger = (
|
|
611
|
+
(unique_a, unique_b) if len(unique_a) <= len(unique_b) else (unique_b, unique_a)
|
|
612
|
+
)
|
|
613
|
+
cross_overlap = sum(
|
|
614
|
+
1 for read_name in smaller if overlap_adjacency.get(read_name, set()) & larger
|
|
615
|
+
)
|
|
616
|
+
shared_rows.append(
|
|
617
|
+
[
|
|
618
|
+
site_a,
|
|
619
|
+
site_b,
|
|
620
|
+
shared,
|
|
621
|
+
len(support_by_site[site_a]),
|
|
622
|
+
len(support_by_site[site_b]),
|
|
623
|
+
cross_overlap,
|
|
624
|
+
1 if shared >= settings.min_shared else 0,
|
|
625
|
+
]
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
classes, site_to_class = assign_region_ids(clusters, shared_counts, settings.min_shared)
|
|
629
|
+
|
|
630
|
+
# 给 cluster 填 region_id
|
|
631
|
+
for cluster in clusters:
|
|
632
|
+
cluster["region_id"] = site_to_class[cluster["site_id"]]
|
|
633
|
+
|
|
634
|
+
fill_segment_end_junction_ids(segment_end_rows, clusters)
|
|
635
|
+
|
|
636
|
+
### 4. 写断点表 / 类表 / read 类别归属 / segment ends ###
|
|
637
|
+
|
|
638
|
+
assignment_rows = []
|
|
639
|
+
for read_name in sorted(classification):
|
|
640
|
+
site_ids = sorted(supported_sites_by_read.get(read_name, set()))
|
|
641
|
+
class_ids = sorted({site_to_class[site_id] for site_id in site_ids})
|
|
642
|
+
assignment_rows.append(
|
|
643
|
+
[
|
|
644
|
+
read_name,
|
|
645
|
+
classification[read_name]["category"],
|
|
646
|
+
"+".join(class_ids) or "-",
|
|
647
|
+
",".join(site_ids) or "-",
|
|
648
|
+
]
|
|
649
|
+
)
|
|
650
|
+
write_tsv(
|
|
651
|
+
os.path.join(settings.outdir, "read_class_assignment.tsv"),
|
|
652
|
+
[
|
|
653
|
+
"read_name",
|
|
654
|
+
"read_category",
|
|
655
|
+
"class_ids",
|
|
656
|
+
"site_ids",
|
|
657
|
+
],
|
|
658
|
+
assignment_rows,
|
|
659
|
+
[
|
|
660
|
+
"class_ids:该 read 支持的断点所属的方案 A 类;载体 reads 没有断点,填 -。",
|
|
661
|
+
"site_ids:该 read 支持的断点(S##/G##a/G##b)。",
|
|
662
|
+
],
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
site_rows = []
|
|
666
|
+
for cluster in sorted(
|
|
667
|
+
clusters, key=lambda item: (item["chrom"], item["boundary_median"], item["site_id"])
|
|
668
|
+
):
|
|
669
|
+
site_rows.append(
|
|
670
|
+
[
|
|
671
|
+
cluster["site_id"],
|
|
672
|
+
cluster["region_id"],
|
|
673
|
+
cluster["chrom"],
|
|
674
|
+
cluster["genome_side"],
|
|
675
|
+
cluster["boundary_min"],
|
|
676
|
+
cluster["boundary_max"],
|
|
677
|
+
cluster["boundary_median"],
|
|
678
|
+
len(cluster["templates"]),
|
|
679
|
+
len(cluster["read_names"]),
|
|
680
|
+
",".join(sorted(cluster["exog_names"])) or "-",
|
|
681
|
+
",".join(sorted(cluster["read_names"])),
|
|
682
|
+
cluster["junction_type"],
|
|
683
|
+
cluster["junction_pair"],
|
|
684
|
+
]
|
|
685
|
+
)
|
|
686
|
+
write_tsv(
|
|
687
|
+
os.path.join(settings.outdir, "breakpoint_sites.tsv"),
|
|
688
|
+
[
|
|
689
|
+
"site_id",
|
|
690
|
+
"region_id",
|
|
691
|
+
"chrom",
|
|
692
|
+
"genome_side",
|
|
693
|
+
"boundary_min",
|
|
694
|
+
"boundary_max",
|
|
695
|
+
"boundary_median",
|
|
696
|
+
"n_events",
|
|
697
|
+
"n_reads",
|
|
698
|
+
"exog_names",
|
|
699
|
+
"read_names",
|
|
700
|
+
"junction_type",
|
|
701
|
+
"junction_pair",
|
|
702
|
+
],
|
|
703
|
+
site_rows,
|
|
704
|
+
[
|
|
705
|
+
"junction_type=vector:基因组末端紧邻外源;genome:基因组末端紧邻另一段基因组。",
|
|
706
|
+
"G##a/G##b 是同一个 genome–genome 断点的两个末端;S## 为 vector 断点。",
|
|
707
|
+
],
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
class_rows = []
|
|
711
|
+
for class_info in classes:
|
|
712
|
+
sites = class_info["sites"]
|
|
713
|
+
chroms = sorted({site["chrom"] for site in sites})
|
|
714
|
+
class_rows.append(
|
|
715
|
+
[
|
|
716
|
+
class_info["class_id"],
|
|
717
|
+
"{0}|{1}".format(class_info["class_id"], ",".join(class_info["site_ids"])),
|
|
718
|
+
",".join(chroms),
|
|
719
|
+
min(site["boundary_min"] for site in sites),
|
|
720
|
+
max(site["boundary_max"] for site in sites),
|
|
721
|
+
len(sites),
|
|
722
|
+
len(class_info["junction_read_names"]),
|
|
723
|
+
",".join(sorted({site["genome_side"] for site in sites})),
|
|
724
|
+
",".join(class_info["site_ids"]),
|
|
725
|
+
",".join(sorted(class_info["exog_names"])) or "-",
|
|
726
|
+
",".join(sorted({site["junction_type"] for site in sites})),
|
|
727
|
+
",".join(sorted(class_info["junction_read_names"])),
|
|
728
|
+
]
|
|
729
|
+
)
|
|
730
|
+
write_tsv(
|
|
731
|
+
os.path.join(settings.outdir, "breakpoint_classes.tsv"),
|
|
732
|
+
[
|
|
733
|
+
"region_id",
|
|
734
|
+
"region_label",
|
|
735
|
+
"chrom",
|
|
736
|
+
"start",
|
|
737
|
+
"end",
|
|
738
|
+
"n_sites",
|
|
739
|
+
"n_reads",
|
|
740
|
+
"genome_sides",
|
|
741
|
+
"site_ids",
|
|
742
|
+
"exog_names",
|
|
743
|
+
"junction_types",
|
|
744
|
+
"read_names",
|
|
745
|
+
],
|
|
746
|
+
class_rows,
|
|
747
|
+
[
|
|
748
|
+
"方案 A:site 之间共享 reads >= --min-shared(默认 2)就合并为一类。",
|
|
749
|
+
"region_id 供 07 丝带图使用;read_names 是类内 junction reads(并集)。",
|
|
750
|
+
],
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
write_tsv(
|
|
754
|
+
os.path.join(settings.outdir, "breakpoint_shared_reads.tsv"),
|
|
755
|
+
[
|
|
756
|
+
"site_a",
|
|
757
|
+
"site_b",
|
|
758
|
+
"n_shared_reads",
|
|
759
|
+
"support_a",
|
|
760
|
+
"support_b",
|
|
761
|
+
"cross_overlap_pairs",
|
|
762
|
+
"merge",
|
|
763
|
+
],
|
|
764
|
+
shared_rows,
|
|
765
|
+
[
|
|
766
|
+
"n_shared_reads:同时支持两个断点的 reads 数。",
|
|
767
|
+
"cross_overlap_pairs:A 独有 reads 与 B 独有 reads 之间的 overlap 对数(支持度参考,不参与方案 A 合并)。",
|
|
768
|
+
"merge=1 表示 n_shared_reads >= --min-shared。",
|
|
769
|
+
],
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
write_tsv(
|
|
773
|
+
os.path.join(settings.outdir, "segment_ends.tsv"),
|
|
774
|
+
[
|
|
775
|
+
"read_name",
|
|
776
|
+
"chrom",
|
|
777
|
+
"segment_start",
|
|
778
|
+
"segment_end",
|
|
779
|
+
"strand",
|
|
780
|
+
"end_side",
|
|
781
|
+
"end_coordinate",
|
|
782
|
+
"genome_side",
|
|
783
|
+
"adjacent_type",
|
|
784
|
+
"adjacent_name",
|
|
785
|
+
"adjacent_coordinate",
|
|
786
|
+
"junction_type",
|
|
787
|
+
"junction_id",
|
|
788
|
+
"gap_bp",
|
|
789
|
+
"window_50bp",
|
|
790
|
+
],
|
|
791
|
+
[
|
|
792
|
+
[
|
|
793
|
+
row[column]
|
|
794
|
+
for column in (
|
|
795
|
+
"read_name",
|
|
796
|
+
"chrom",
|
|
797
|
+
"segment_start",
|
|
798
|
+
"segment_end",
|
|
799
|
+
"strand",
|
|
800
|
+
"end_side",
|
|
801
|
+
"end_coordinate",
|
|
802
|
+
"genome_side",
|
|
803
|
+
"adjacent_type",
|
|
804
|
+
"adjacent_name",
|
|
805
|
+
"adjacent_coordinate",
|
|
806
|
+
"junction_type",
|
|
807
|
+
"junction_id",
|
|
808
|
+
"gap_bp",
|
|
809
|
+
"window_50bp",
|
|
810
|
+
)
|
|
811
|
+
]
|
|
812
|
+
for row in segment_end_rows
|
|
813
|
+
],
|
|
814
|
+
[
|
|
815
|
+
"每个 genome 片段的左右末端一行;adjacent_type 看的是 read 轴上紧邻的片段。",
|
|
816
|
+
"非朝向载体的末端也会列出(junction_type=genome/none),不会被误当 vector 断点。",
|
|
817
|
+
],
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
### 5. 每类 read 池与 FASTQ ###
|
|
821
|
+
|
|
822
|
+
vector_read_names = {
|
|
823
|
+
read_name
|
|
824
|
+
for read_name, info in classification.items()
|
|
825
|
+
if info["category"] == "vector_region"
|
|
826
|
+
}
|
|
827
|
+
class_pool_rows = []
|
|
828
|
+
read_names_by_output = {}
|
|
829
|
+
for class_info in classes:
|
|
830
|
+
class_id = class_info["class_id"]
|
|
831
|
+
class_dir = os.path.join(settings.outdir, "classes", class_id)
|
|
832
|
+
os.makedirs(class_dir, exist_ok=True)
|
|
833
|
+
junction_reads = class_info["junction_read_names"]
|
|
834
|
+
all_reads = junction_reads | vector_read_names
|
|
835
|
+
supported_vector = sum(
|
|
836
|
+
1
|
|
837
|
+
for vector_read in vector_read_names
|
|
838
|
+
if overlap_adjacency.get(vector_read, set()) & junction_reads
|
|
839
|
+
)
|
|
840
|
+
pool_bp = sum(classification.get(read_name, {}).get("length", 0) for read_name in all_reads)
|
|
841
|
+
reads_fastq = os.path.join(class_dir, "reads.fastq")
|
|
842
|
+
class_pool_rows.append(
|
|
843
|
+
[
|
|
844
|
+
class_id,
|
|
845
|
+
len(junction_reads),
|
|
846
|
+
len(vector_read_names),
|
|
847
|
+
len(all_reads),
|
|
848
|
+
pool_bp,
|
|
849
|
+
supported_vector,
|
|
850
|
+
reads_fastq,
|
|
851
|
+
]
|
|
852
|
+
)
|
|
853
|
+
read_names_by_output[reads_fastq] = all_reads
|
|
854
|
+
read_names_by_output[os.path.join(class_dir, "junction_reads.fastq")] = junction_reads
|
|
855
|
+
read_names_by_output[os.path.join(class_dir, "vector_reads.fastq")] = vector_read_names
|
|
856
|
+
|
|
857
|
+
write_tsv(
|
|
858
|
+
os.path.join(settings.outdir, "class_read_pools.tsv"),
|
|
859
|
+
[
|
|
860
|
+
"class_id",
|
|
861
|
+
"n_junction_reads",
|
|
862
|
+
"n_vector_reads",
|
|
863
|
+
"n_total_reads",
|
|
864
|
+
"pool_read_bp",
|
|
865
|
+
"n_vector_with_overlap_support",
|
|
866
|
+
"reads_fastq",
|
|
867
|
+
],
|
|
868
|
+
class_pool_rows,
|
|
869
|
+
[
|
|
870
|
+
"类池 = 该类断点的 junction reads(并集) + 全部载体区域 reads。",
|
|
871
|
+
"pool_read_bp:类池所有 reads 长度之和(不是组装长度)。",
|
|
872
|
+
],
|
|
873
|
+
)
|
|
874
|
+
write_fastq_subsets(settings.reads, read_names_by_output)
|
|
875
|
+
|
|
876
|
+
### 6. 摘要 ###
|
|
877
|
+
|
|
878
|
+
summary_lines = []
|
|
879
|
+
summary_lines.append(
|
|
880
|
+
"断点图方案 A:site 合并阈值 >= {0} 条共享 reads".format(settings.min_shared)
|
|
881
|
+
)
|
|
882
|
+
summary_lines.append("sites: {0}".format(len(clusters)))
|
|
883
|
+
summary_lines.append("classes: {0}".format(len(classes)))
|
|
884
|
+
summary_lines.append(
|
|
885
|
+
"genome–genome 配对: {0}".format(
|
|
886
|
+
"; ".join(
|
|
887
|
+
"{0}({1})".format(pair_name, ",".join(site["site_id"] for site in pair_clusters))
|
|
888
|
+
for pair_clusters, pair_name in genome_pairs
|
|
889
|
+
)
|
|
890
|
+
or "-"
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
for class_info in classes:
|
|
894
|
+
summary_lines.append(
|
|
895
|
+
" {0}: {1} (junction reads {2})".format(
|
|
896
|
+
class_info["class_id"],
|
|
897
|
+
",".join(class_info["site_ids"]),
|
|
898
|
+
len(class_info["junction_read_names"]),
|
|
899
|
+
)
|
|
900
|
+
)
|
|
901
|
+
with open(os.path.join(settings.outdir, "graph_summary.txt"), "w") as summary_file:
|
|
902
|
+
summary_file.write("\n".join(summary_lines) + "\n")
|
|
903
|
+
print("\n".join(summary_lines))
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
"outdir": settings.outdir,
|
|
907
|
+
"sites": len(clusters),
|
|
908
|
+
"classes": len(classes),
|
|
909
|
+
"segment_ends": len(segment_end_rows),
|
|
910
|
+
}
|