SLRanger 0.0.1.9__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.
SLRanger/SL_detect.py ADDED
@@ -0,0 +1,744 @@
1
+ #!/usr/bin/env python
2
+ import re
3
+ import argparse
4
+ import pysam
5
+ import pandas as pd
6
+ # from ssw import AlignmentMgr
7
+ from pyssw.ssw_wrap import Aligner
8
+ from Bio.Seq import Seq
9
+ import random
10
+ import multiprocessing
11
+ from tqdm import tqdm
12
+ import time
13
+ from SLRanger.visualization import visualize_html
14
+
15
+ def fasta_to_dict(fasta_path):
16
+ fasta_dict = {}
17
+ with open(fasta_path, 'r') as f:
18
+ current_key = None
19
+ sequence_parts = []
20
+ for line in f:
21
+ line = line.strip()
22
+ if line.startswith('>'):
23
+ if current_key:
24
+ # 保存上一个序列
25
+ fasta_dict[current_key] = ''.join(sequence_parts)
26
+ current_key = line[1:] # 去掉 '>'
27
+ sequence_parts = []
28
+ else:
29
+ sequence_parts.append(line)
30
+ # 保存最后一个序列
31
+ if current_key:
32
+ fasta_dict[current_key] = ''.join(sequence_parts)
33
+ return fasta_dict
34
+
35
+ def generate_mismatches(kmer, bases=['A', 'T', 'C', 'G']):
36
+ mismatches = set()
37
+ for i in range(len(kmer)):
38
+ for base in bases:
39
+ if base != kmer[i]:
40
+ mismatched_kmer = kmer[:i] + base + kmer[i + 1:]
41
+ mismatches.add(mismatched_kmer)
42
+ return mismatches
43
+
44
+ def build_mismatch_index(sequences, k):
45
+ mismatch_to_kmer = {}
46
+
47
+ for seq_id, seq in sequences.items():
48
+ for i in range(len(seq) - k + 1):
49
+ kmer = seq[i:i + k]
50
+ mismatches = generate_mismatches(kmer)
51
+ for mismatch in mismatches:
52
+ if mismatch not in mismatch_to_kmer:
53
+ mismatch_to_kmer[mismatch] = kmer
54
+
55
+ return mismatch_to_kmer
56
+
57
+ def get_sequences_by_length(dictionary):
58
+ # 按键排序
59
+ sorted_keys = sorted(dictionary.keys())
60
+ # 用于跟踪已处理的长度
61
+ seen_lengths = set()
62
+ # 结果字典
63
+ result = {}
64
+
65
+ # 遍历排序后的键
66
+ for key in sorted_keys:
67
+ sequence = dictionary[key]
68
+ seq_length = len(sequence)
69
+ # 如果该长度尚未处理,添加到结果
70
+ if seq_length not in seen_lengths:
71
+ result[key] = {
72
+ 'sequence': sequence, # 原始序列
73
+ 'length': seq_length # 序列长度
74
+ }
75
+ seen_lengths.add(seq_length)
76
+
77
+ return result
78
+
79
+ def extract_kmers(sequences, k):
80
+ kmer_id = {}
81
+ # 遍历每个序列并为每个5-mer分配ID
82
+ for key, seq in sequences.items():
83
+ encoded_seq = []
84
+ for i in range(len(seq) - k + 1):
85
+ kmer = seq[i:i+k]
86
+ encoded_seq.append(kmer)
87
+ kmer_id[key] = encoded_seq
88
+ return kmer_id
89
+
90
+ def find_matches(query, mismatch_to_kmer, kmer_to_refs, k):
91
+ query_kmers = [query[i:i + k] for i in range(len(query) - k + 1)]
92
+ matches = []
93
+
94
+ for query_kmer in query_kmers:
95
+ if query_kmer in mismatch_to_kmer:
96
+ original_kmer = mismatch_to_kmer[query_kmer]
97
+ if original_kmer in kmer_to_refs:
98
+ match_info = kmer_to_refs[original_kmer]
99
+ matches.extend(match_info)
100
+
101
+ return matches
102
+
103
+ def longest_consecutive(nums):
104
+ num_set = set(nums)
105
+ longest_streak = 0
106
+ max_end_value = None
107
+
108
+ for num in num_set:
109
+ if num - 1 not in num_set:
110
+ current_num = num
111
+ current_streak = 1
112
+
113
+ while current_num + 1 in num_set:
114
+ current_num += 1
115
+ current_streak += 1
116
+
117
+ if current_streak > longest_streak:
118
+ longest_streak = current_streak
119
+ max_end_value = current_num
120
+
121
+ return longest_streak, max_end_value
122
+
123
+ def find_best_match(query, mismatch_to_kmer, kmer_to_refs, k):
124
+ encoded_query = [query[i:i+k] for i in range(len(query) - k + 1)]
125
+ ref_positions = []
126
+ max_consecutive = 0
127
+ best_consecutive_end = 0
128
+
129
+ for query_kmer in encoded_query:
130
+ if query_kmer in kmer_to_refs:
131
+ if kmer_to_refs.count(query_kmer) > 1:
132
+ ref_positions.extend(index for index, kmer in enumerate(kmer_to_refs) if kmer == query_kmer)
133
+ else:
134
+ ref_positions.append(kmer_to_refs.index(query_kmer))
135
+ elif query_kmer in mismatch_to_kmer:
136
+ original_kmer = mismatch_to_kmer[query_kmer]
137
+ if original_kmer in kmer_to_refs:
138
+ if kmer_to_refs.count(query_kmer) > 1:
139
+ ref_positions.extend(index for index, kmer in enumerate(kmer_to_refs) if kmer == original_kmer)
140
+ else:
141
+ ref_positions.append(kmer_to_refs.index(original_kmer))
142
+ max_intersection = len(set(ref_positions))
143
+ if max_intersection > 0:
144
+ max_consecutive, best_consecutive_end = longest_consecutive(ref_positions)
145
+
146
+ if max_consecutive > len(query) - k:
147
+ # if max_intersection > max_consecutive:
148
+ max_intersection = max_consecutive
149
+
150
+ return max_intersection, max_consecutive, best_consecutive_end
151
+
152
+ def soft_processed(sequence, strand, cigar):
153
+ """
154
+ 根据 strand 和 cigar 信息提取指定长度的序列部分。
155
+ 为direct RNA设置
156
+ 参数:
157
+ - sequence (str): 原始的序列字符串。这个sequence已经被reverse处理过了,与正链一致,所以取序列的时候都从开头取起
158
+ - strand (str): 链方向,"+" 或 "-"。
159
+ - cigar (list of tuples): CIGAR 信息,表示为列表的元组 (operation, length)。
160
+ 例如 [(4, 5), (0, 50)] 表示 5 bp 的 soft clipping,接着是 50 bp 的匹配。
161
+ 返回:
162
+ - str: 根据条件截取后的序列。再多加两个碱基在后面
163
+ """
164
+ if not cigar:
165
+ return "" # Return empty string if no CIGAR info
166
+
167
+ if strand == "+":
168
+ if cigar[0][0] == 4:
169
+ soft_clip_length = cigar[0][1]
170
+ # 返回从序列开头截取 soft_clip_length 长度的序列部分
171
+ return sequence[:soft_clip_length + 2] #
172
+ elif strand == "-":
173
+ if cigar[-1][0] == 4:
174
+ soft_clip_length = cigar[-1][1]
175
+ # 返回从序列开头截取 soft_clip_length 长度的序列部分
176
+ return sequence[:soft_clip_length + 2] #
177
+ else:
178
+ # 不符合条件时,返回原始序列
179
+ return ""
180
+
181
+ def soft_extract(sequence, cigar):
182
+ """
183
+ 根据 strand 和 cigar 信息提取指定长度的序列部分。
184
+ 取两头,为双链cDNA而设置
185
+ 参数:
186
+ - sequence (str): 原始的序列字符串
187
+ - cigar (list of tuples): CIGAR 信息,表示为列表的元组 (operation, length)。
188
+ 例如 [(4, 5), (0, 50)] 表示 5 bp 的 soft clipping,接着是 50 bp 的匹配。
189
+ 返回:
190
+ - str: 根据条件截取后的序列。再多加两个碱基在后面
191
+ """
192
+ seq_5 = []
193
+ query_re_3 = []
194
+ if cigar[0][0] == 4:
195
+ soft_clip_length = cigar[0][1]
196
+ # 返回从序列开头截取 soft_clip_length 长度的序列部分
197
+ seq_5 = sequence[:soft_clip_length + 2] #
198
+
199
+ if cigar[-1][0] == 4:
200
+ soft_clip_length = cigar[-1][1]
201
+ # 返回从序列尾部截取 soft_clip_length 长度的序列部分
202
+ seq_3 = sequence[-(soft_clip_length + 2):] #
203
+ query_re_3 = str(Seq(seq_3).reverse_complement())
204
+
205
+ return seq_5, query_re_3
206
+
207
+ def ssw_wrapper(seq1, seq2, match=1, mismatch=1, gap_open=1, gap_extend=1):
208
+ """
209
+ parameter are write inside the function
210
+ seq1 is ref and seq2 is query
211
+ # todo : leave a api to change matrix
212
+ """
213
+
214
+ ref_seq = str(seq1)
215
+ read_seq = str(seq2)
216
+ # reduce the gap open score from 3 to 1 for nanopore reads
217
+ aligner = Aligner(ref_seq,
218
+ match, mismatch, gap_open, gap_extend,
219
+ report_cigar=True, )
220
+
221
+ aln = aligner.align(read_seq) # min_score=20, min_len=10)
222
+
223
+ return aln
224
+
225
+ def consensus(ref, query, cigar_string, ref_shift=0, query_shift = 0):
226
+ """
227
+ to get a concensus sequence as the longest cons of both sequence,
228
+ and fill all disagreement with N
229
+ for example,
230
+ query:AAATA-TAGAA
231
+ ref: AAACACTA-AA
232
+ cons: AAANANCANAA
233
+
234
+ use query to get the con
235
+ then get two chain file, 0 based
236
+ """
237
+
238
+ # pattern = re.compile('([0-9]*)([MIDNSHP=X])')
239
+ pattern = re.compile(r"([0-9]+)b?'?([MIDNSHP=X])")
240
+ query_out = []
241
+ ref_out = []
242
+ cons = []
243
+
244
+ query_pos = query_shift
245
+ ref_pos = ref_shift
246
+
247
+ cigar_pattern = pattern.findall(cigar_string)
248
+ for i, (length, code) in enumerate(cigar_pattern):
249
+ if i == 0 or i == len(cigar_pattern) - 1:
250
+ if code == "S":
251
+ continue
252
+
253
+ length = int(length)
254
+
255
+ if code == "M":
256
+ for i in range(length):
257
+ q_s = query[query_pos].upper()
258
+ try:
259
+ r_s = ref[ref_pos].upper()
260
+ except IndexError as e:
261
+ print
262
+ "indexerror", ref_pos
263
+
264
+ query_out.append(q_s)
265
+ ref_out.append(r_s)
266
+
267
+ cons.append(q_s) if q_s == r_s else cons.append("m")
268
+
269
+ ref_pos += 1
270
+ query_pos += 1
271
+
272
+ elif code in "D":
273
+ for i in range(length):
274
+ r_s = ref[ref_pos]
275
+
276
+ ref_out.append(r_s)
277
+ query_out.append("-")
278
+ cons.append("d")
279
+
280
+ ref_pos += 1
281
+
282
+ elif code in "IHS":
283
+ for i in range(length):
284
+ q_s = query[query_pos]
285
+
286
+ query_out.append(q_s)
287
+ ref_out.append("-")
288
+ cons.append("i")
289
+ query_pos += 1
290
+
291
+ return "".join(cons) # "".join(cons), "".join(query_out), "".join(ref_out)
292
+
293
+ def drs_score_calculate(sw_score, max_intersection, max_consecutive, ref_end, ref_length, random_seq_len, seq_start, soft_length):
294
+ # factor = get_value(best_end, ref_length - k)
295
+
296
+ score_seq = (1 - seq_start/soft_length) # * (seq_length/soft_length) # 在soft clipping中的位置,越靠头部越好
297
+ # score_ref = 1 - (ref_length - ref_end)/ref_length
298
+ score_ref = 1 - (ref_length - ref_end)/(random_seq_len + 3) #(SL1_len - k)*2
299
+ score_kmer = 0.5 * (max_intersection + max_consecutive - 1)
300
+ score = score_seq * score_ref * (sw_score + score_kmer)
301
+
302
+ final_score = max(score, 0)
303
+ return final_score
304
+
305
+ def cdna_score_calculate(sw_score, max_intersection, max_consecutive, ref_end, ref_length, random_seq_len, seq_end, soft_length):
306
+ # factor = get_value(best_end, ref_length - k)
307
+
308
+ score_seq = seq_end/soft_length # * (seq_length/soft_length) # 在soft clipping中的位置,越靠alignment region越好
309
+ # score_ref = 1 - (ref_length - ref_end)/ref_length
310
+ score_ref = 1 - (ref_length - ref_end)/(random_seq_len + 3) #(SL1_len - k)*2
311
+ score_kmer = 0.5 * (max_intersection + max_consecutive - 1)
312
+ score = score_seq * score_ref * (sw_score + score_kmer)
313
+
314
+ final_score = max(score, 0)
315
+ return final_score
316
+
317
+ def ref_score_calculate(sw_score, max_intersection, max_consecutive):
318
+ score_kmer = 0.5 * (max_intersection + max_consecutive - 1)
319
+ score = (sw_score + score_kmer)
320
+
321
+ final_score = max(score, 0)
322
+ return final_score
323
+
324
+ def length_index(SL, ref_seq, kmer, mismatch_to_kmer, random_seq_len, k):
325
+ dict = {}
326
+
327
+ for i in range(k, len(ref_seq) + 1):
328
+ ref_seq_s = ref_seq[-i:]
329
+ sw_aln = ssw_wrapper(ref_seq, ref_seq_s) # score>10,
330
+ sw_score = sw_aln.score
331
+ # ref_start = sw_aln.ref_begin
332
+ # ref_end = sw_aln.ref_end
333
+ # read_start = sw_aln.query_begin
334
+ read_end = sw_aln.query_end + 1
335
+ # sw_cigar = sw_aln.cigar_string
336
+
337
+ corrected_sequence_sw = ref_seq_s
338
+ # last_three_chars = corrected_sequence_sw[-3:]
339
+ seq_s_length = len(corrected_sequence_sw)
340
+ ref_length = len(ref_seq)
341
+ # seq_length = read_end - read_start
342
+ kmer_to_refs = kmer[SL]
343
+ max_intersection, max_consecutive, best_end = find_best_match(corrected_sequence_sw, mismatch_to_kmer,
344
+ kmer_to_refs, k)
345
+ final_score = ref_score_calculate(sw_score, max_intersection, max_consecutive)
346
+ dict[i] = final_score
347
+
348
+ return dict
349
+
350
+ def final_score_process(final_score, ref_length, seq_s_length, length_score):
351
+ if seq_s_length <= ref_length:
352
+ SL_score = final_score * (final_score / length_score[seq_s_length])
353
+ # final_score_normalized = 100 * (final_score / length_score[seq_s_length])
354
+ else:
355
+ SL_score = final_score * (final_score / (seq_s_length / ref_length * length_score[ref_length]))
356
+ # final_score_normalized = 100 * (final_score / (seq_s_length / ref_length * length_score[ref_length]))
357
+ return SL_score # final_score_normalized
358
+
359
+ def random_score(random_sequences_dict, random_kmer, random_mismatch_to_kmer,length_scores, random_seq_len, corrected_sequence, k):
360
+ random_sw_score_max = 0
361
+ random_final_score_max = 0
362
+ random_SL_score_max = 0
363
+ length_score = length_scores[random_seq_len]
364
+ for key, random_seq in random_sequences_dict.items():
365
+ random_length_score = length_score
366
+ random_aln_sw = ssw_wrapper(random_seq, corrected_sequence)
367
+ random_sw_score = random_aln_sw.score
368
+ random_read_start = random_aln_sw.query_begin
369
+ random_read_end = random_aln_sw.query_end + 1
370
+ corrected_sequence_sw = corrected_sequence[random_read_start:random_read_end]
371
+ seq_s_length = len(corrected_sequence_sw)
372
+
373
+ if seq_s_length >= 5:
374
+ random_ref_end = random_aln_sw.ref_end + 1
375
+ random_ref_length = len(random_seq)
376
+ kmer_to_refs = random_kmer[key]
377
+ max_intersection, max_consecutive, best_end = find_best_match(corrected_sequence_sw,
378
+ random_mismatch_to_kmer,
379
+ kmer_to_refs, k)
380
+ if mode == 'RNA':
381
+ random_final_score = drs_score_calculate(random_sw_score, max_intersection, max_consecutive, random_ref_end,
382
+ random_ref_length, random_seq_len, random_read_start, len(corrected_sequence))
383
+ else:
384
+ random_final_score = cdna_score_calculate(random_sw_score, max_intersection, max_consecutive, random_ref_end,
385
+ random_ref_length, random_seq_len, random_read_end, len(corrected_sequence))
386
+ random_SL_score = final_score_process(random_final_score, random_ref_length,
387
+ seq_s_length, random_length_score)
388
+ if random_SL_score > random_SL_score_max:
389
+ random_sw_score_max = random_sw_score
390
+ random_final_score_max = random_final_score
391
+ random_SL_score_max = random_SL_score
392
+
393
+ return random_sw_score_max, random_final_score_max, random_SL_score_max
394
+
395
+ def update(*a):
396
+ if a[0] is not None:
397
+ outfile.write(a[0])
398
+ pbar.update(1)
399
+
400
+ def drs_calculation_per_process(item,sl_dict,length_scores,random_sequences_dict,random_seq_len,random_kmer,
401
+ random_mismatch_to_kmer,k,kmer,mismatch_to_kmer):
402
+ query_name = item[0]
403
+ full_query_sequence = item[1]
404
+ strand = item[2]
405
+ if strand == '-':
406
+ query_seq = str(Seq(full_query_sequence).reverse_complement()) # 序列映射到负链,需要反向互补处理
407
+ else:
408
+ query_seq = full_query_sequence # 序列映射到正链,直接使用
409
+
410
+ corrected_sequence = soft_processed(query_seq, strand, item[3])
411
+ aligned_len = item[4]
412
+
413
+ if corrected_sequence is not None:
414
+ soft_length = len(corrected_sequence)
415
+ mes = query_name + '\t' + strand + '\t' + str(soft_length) + '\t' + str(aligned_len) + '\t' # with soft_processed
416
+ else:
417
+ soft_length = 0
418
+ mes = query_name + '\t' + strand + '\t' + 'NA' + '\t' + str(aligned_len) + '\t' # with soft_processed
419
+
420
+ if soft_length < k: # 太短了就不要了
421
+ SL_sw_dict = {'read_end': 'NA', 'query_length': 'NA',
422
+ 'consensus': 'NA', 'random_sw_score': 'NA',
423
+ 'random_final_score': 'NA', 'random_SL_score': 'NA',
424
+ 'sw_score': 'NA', 'final_score': 'NA',
425
+ 'SL_score': 'NA'}
426
+ mes = mes + '\t'.join([str(value) for value in SL_sw_dict.values()]) + '\t' + 'random' + "\n"
427
+ return mes
428
+
429
+ ### use SL1 seq for SW check
430
+ SL_sw_dict = {}
431
+ for SL, SEQ in sl_dict.items():
432
+ length_score = length_scores[len(SEQ)]
433
+ # corrected_sequence = 'CAAG'
434
+ sw_aln = ssw_wrapper(SEQ, corrected_sequence) # score>10,
435
+ sw_score = sw_aln.score
436
+ ref_start = sw_aln.ref_begin
437
+ ref_end = sw_aln.ref_end + 1
438
+ read_start = sw_aln.query_begin
439
+ read_end = sw_aln.query_end + 1
440
+ sw_cigar = sw_aln.cigar_string
441
+
442
+ corrected_sequence_sw = corrected_sequence[read_start:read_end]
443
+
444
+ seq_s_length = len(corrected_sequence_sw)
445
+ random_sw_score_max, random_final_score_max, random_SL_score_max = random_score(random_sequences_dict,
446
+ random_kmer,
447
+ random_mismatch_to_kmer,
448
+ length_scores, random_seq_len,
449
+ corrected_sequence, k)
450
+ if seq_s_length >= k:
451
+ sw_ref = SEQ[ref_start:ref_end]
452
+
453
+ # print("\n".join(consensus(corrected_sequence_sw, sw_ref, sw_cigar, 0)))
454
+ # last_three_chars = corrected_sequence_sw[-3:]
455
+ con_seq = consensus(sw_ref, corrected_sequence_sw, sw_cigar, 0)
456
+ ref_length = len(SEQ)
457
+ kmer_to_refs = kmer[SL]
458
+ max_intersection, max_consecutive, best_end = find_best_match(corrected_sequence_sw, mismatch_to_kmer,
459
+ kmer_to_refs, k)
460
+ final_score = drs_score_calculate(sw_score, max_intersection, max_consecutive, ref_end, ref_length,
461
+ random_seq_len, read_start, soft_length) # sw_score, max_intersection, max_consecutive, ref_end, ref_length, seq_start, seq_length, soft_length
462
+ SL_score = final_score_process(final_score, ref_length, seq_s_length, length_score)
463
+
464
+ SL_sw_dict[SL] = {'read_end': read_end,
465
+ 'query_length': seq_s_length,
466
+ 'consensus': con_seq,
467
+ 'random_sw_score': random_sw_score_max,
468
+ 'random_final_score': round(random_final_score_max, 2),
469
+ 'random_SL_score': round(random_SL_score_max, 2),
470
+ 'sw_score': sw_score,
471
+ 'final_score': round(final_score, 2),
472
+ 'SL_score': round(SL_score, 2)
473
+ }
474
+ else:
475
+ SL_sw_dict[SL] = {'read_end': read_end,
476
+ 'query_length': None,
477
+ 'consensus': None,
478
+ 'random_sw_score': 0,
479
+ 'random_final_score': 0,
480
+ 'random_SL_score': 0,
481
+ 'sw_score': 0,
482
+ 'final_score': 0,
483
+ 'SL_score': 0
484
+ }
485
+
486
+ SL_sw_df = pd.DataFrame.from_dict(SL_sw_dict, orient='index')
487
+ SL_sw_df_s = SL_sw_df[SL_sw_df['SL_score'] > SL_sw_df['random_SL_score']]
488
+ sl_max = SL_sw_df_s['SL_score'].max()
489
+ # filtered_df = SL_sw_df_s[SL_sw_df_s['SL_score'] == sl_max]
490
+ filtered_df = SL_sw_df_s[SL_sw_df_s['SL_score'] == sl_max]
491
+
492
+ if len(filtered_df) > 1:
493
+ mes = mes + '\t'.join([str(value) for value in filtered_df.iloc[0]]) + '\t' + filtered_df.index[
494
+ 0] + '_unknown' + "\n"
495
+ elif len(filtered_df) == 1:
496
+ mes = mes + '\t'.join([str(value) for value in filtered_df.iloc[0]]) + '\t' + filtered_df.index[0] + "\n"
497
+ else:
498
+ filtered_df_s = SL_sw_df[SL_sw_df['SL_score'] == SL_sw_df['SL_score'].max()]
499
+ mes = mes + '\t'.join([str(value) for value in filtered_df_s.iloc[0]]) + '\t' + 'random' + "\n"
500
+ return mes
501
+
502
+ def cdna_calculation_per_process(item,sl_dict,length_scores,random_sequences_dict,random_seq_len,random_kmer,
503
+ random_mismatch_to_kmer,k,kmer,mismatch_to_kmer):
504
+ query_name = item[0]
505
+ query_seq = item[1]
506
+ strand = item[2]
507
+ aligned_len = item[4]
508
+ seq_5, seq_3 = soft_extract(query_seq, item[3])
509
+ candidate_seq = [seq_5, seq_3]
510
+
511
+ if seq_5 is None and seq_3 is None:
512
+ mes = query_name + '\t' + strand + '\t' + 'NA' + str(aligned_len) + '\t' # with soft_processed
513
+ SL_sw_dict = {'read_end': 'NA', 'query_length': 'NA',
514
+ 'consensus': 'NA', 'random_sw_score': 'NA',
515
+ 'random_final_score': 'NA', 'random_SL_score': 'NA',
516
+ 'sw_score': 'NA', 'final_score': 'NA',
517
+ 'SL_score': 'NA'}
518
+ mes = mes + '\t'.join([str(value) for value in SL_sw_dict.values()]) + '\t' + 'random' + "\n"
519
+ return mes
520
+ elif len(seq_5) < 5 and len(seq_3) < 5:
521
+ soft_length = max(len(seq_5), len(seq_3))
522
+ mes = query_name + '\t' + strand + '\t' + str(soft_length) + str(aligned_len) + '\t' # with soft_processed
523
+ SL_sw_dict = {'read_end': 'NA', 'query_length': 'NA',
524
+ 'consensus': 'NA', 'random_sw_score': 'NA',
525
+ 'random_final_score': 'NA', 'random_SL_score': 'NA',
526
+ 'sw_score': 'NA', 'final_score': 'NA',
527
+ 'SL_score': 'NA'}
528
+ mes = mes + '\t'.join([str(value) for value in SL_sw_dict.values()]) + '\t' + 'random' + "\n"
529
+ return mes
530
+
531
+ results = []
532
+ for corrected_sequence in candidate_seq:
533
+ soft_length = len(corrected_sequence)
534
+ ### use SL1 seq for SW check
535
+ SL_sw_dict = {}
536
+ for SL, SEQ in sl_dict.items():
537
+ length_score = length_scores[len(SEQ)]
538
+ # corrected_sequence = 'CAAG'
539
+ sw_aln = ssw_wrapper(SEQ, corrected_sequence) # score>10,
540
+ sw_score = sw_aln.score
541
+ ref_start = sw_aln.ref_begin
542
+ ref_end = sw_aln.ref_end
543
+ read_start = sw_aln.query_begin
544
+ read_end = sw_aln.query_end + 1
545
+ sw_cigar = sw_aln.cigar_string
546
+
547
+ corrected_sequence_sw = corrected_sequence[read_start:read_end]
548
+
549
+ seq_s_length = len(corrected_sequence_sw)
550
+ random_sw_score_max, random_final_score_max, random_SL_score_max = random_score(random_sequences_dict,
551
+ random_kmer,
552
+ random_mismatch_to_kmer,
553
+ length_scores,
554
+ random_seq_len,
555
+ corrected_sequence, k)
556
+ if seq_s_length >= k:
557
+ sw_ref = SEQ[ref_start:ref_end]
558
+
559
+ # print("\n".join(consensus(corrected_sequence_sw, sw_ref, sw_cigar, 0)))
560
+ # last_three_chars = corrected_sequence_sw[-3:]
561
+ con_seq = consensus(sw_ref, corrected_sequence_sw, sw_cigar, 0)
562
+ ref_length = len(SEQ)
563
+ kmer_to_refs = kmer[SL]
564
+ max_intersection, max_consecutive, best_end = find_best_match(corrected_sequence_sw, mismatch_to_kmer,
565
+ kmer_to_refs, k)
566
+ final_score = cdna_score_calculate(sw_score, max_intersection, max_consecutive, ref_end, ref_length,
567
+ random_seq_len, read_end, soft_length) # sw_score, max_intersection, max_consecutive, ref_end, ref_length, seq_start, seq_length, soft_length
568
+ SL_score = final_score_process(final_score, ref_length, seq_s_length, length_score)
569
+
570
+ SL_sw_dict[SL] = {'read_end': read_end,
571
+ 'query_length': seq_s_length,
572
+ 'consensus': con_seq,
573
+ 'random_sw_score': random_sw_score_max,
574
+ 'random_final_score': round(random_final_score_max, 2),
575
+ 'random_SL_score': round(random_SL_score_max, 2),
576
+ 'sw_score': sw_score,
577
+ 'final_score': round(final_score, 2),
578
+ 'SL_score': round(SL_score, 2)
579
+ }
580
+ else:
581
+ SL_sw_dict[SL] = {'read_end': read_end,
582
+ 'query_length': None,
583
+ 'consensus': None,
584
+ 'random_sw_score': 0,
585
+ 'random_final_score': 0,
586
+ 'random_SL_score': 0,
587
+ 'sw_score': 0,
588
+ 'final_score': 0,
589
+ 'SL_score': 0
590
+ }
591
+
592
+ SL_sw_df = pd.DataFrame.from_dict(SL_sw_dict, orient='index')
593
+ SL_sw_df_s = SL_sw_df[SL_sw_df['SL_score'] > SL_sw_df['random_SL_score']]
594
+ sl_max = 0 if pd.isna(SL_sw_df_s['SL_score'].max()) else SL_sw_df_s['SL_score'].max()
595
+ sl_max_f = SL_sw_df['SL_score'].max()
596
+ filtered_df = SL_sw_df_s[SL_sw_df_s['SL_score'] == sl_max]
597
+ filtered_df_f = SL_sw_df[SL_sw_df['SL_score'] == sl_max_f]
598
+ results.append({
599
+ "soft_length": soft_length,
600
+ "sl_max": sl_max,
601
+ "sl_max_f": sl_max_f,
602
+ "filtered_df": filtered_df,
603
+ "filtered_df_f": filtered_df_f
604
+ })
605
+ best_filtered_df = []
606
+ best_filtered_df_f = []
607
+ if len(results) == 1:
608
+ # 只有一个非空序列,直接使用其 filtered_df
609
+ soft_length = results[0]['soft_length']
610
+ best_filtered_df = results[0]["filtered_df"]
611
+ elif len(results) == 2:
612
+ seq1, seq2 = results
613
+ # 情况 1: 至少有一个 sl_max > 0,选择 sl_max 较大的
614
+ if seq1["sl_max"] > 0 or seq2["sl_max"] > 0:
615
+ if seq1["sl_max"] > seq2["sl_max"]:
616
+ best_filtered_df = seq1["filtered_df"]
617
+ soft_length = seq1["soft_length"]
618
+ else:
619
+ best_filtered_df = seq2["filtered_df"]
620
+ soft_length = seq2["soft_length"]
621
+ # 情况 2: 两个 sl_max <= 0,比较 sl_max_f,选择 sl_max_f 较小的
622
+ else:
623
+ if seq1["sl_max_f"] >= seq2["sl_max_f"]:
624
+ best_filtered_df_f = seq1["filtered_df_f"]
625
+ soft_length = seq1["soft_length"]
626
+ else:
627
+ best_filtered_df_f = seq2["filtered_df_f"]
628
+ soft_length = seq2["soft_length"]
629
+
630
+ mes = query_name + '\t' + strand + '\t' + str(soft_length) + '\t' + str(aligned_len) + '\t' # with soft_processed
631
+ if len(best_filtered_df) > 1:
632
+ mes = mes + '\t'.join([str(value) for value in best_filtered_df.iloc[0]]) + '\t' + best_filtered_df.index[
633
+ 0] + '_unknown' + "\n"
634
+ elif len(best_filtered_df) == 1:
635
+ mes = mes + '\t'.join([str(value) for value in best_filtered_df.iloc[0]]) + '\t' + best_filtered_df.index[
636
+ 0] + "\n"
637
+ else:
638
+ mes = mes + '\t'.join([str(value) for value in best_filtered_df_f.iloc[0]]) + '\t' + 'random' + "\n"
639
+
640
+ return mes
641
+
642
+ def main(args):
643
+ global outfile, pbar, mode
644
+ """
645
+ SW comparison between SL1 and SL2
646
+ read reads in bam
647
+ write out a dataframe that including:
648
+ query name, 22nt sequence, SW score, SL1 score, SL2 score, SL1 cigar, SL2 cigar ,SL type
649
+ query name, selected 22nt sequence with soft clipping...
650
+ """
651
+ mode = args.mode
652
+ sl_dict = fasta_to_dict(args.refer)
653
+
654
+ # 生成10个长度为SL1长度的碱基的随机序列
655
+ ref_lengths = [len(key) for key in sl_dict.values()]
656
+ random_seq_len = round(sum(ref_lengths) / len(ref_lengths))
657
+ random.seed(826)
658
+ random_sequences_dict = {}
659
+ for i in range(10):
660
+ random_sequence = ''.join([random.choice('AGTC') for _ in range(random_seq_len)])
661
+ random_sequences_dict[i] = random_sequence
662
+
663
+ k = 5
664
+ kmer = extract_kmers(sl_dict, k)
665
+ mismatch_to_kmer = build_mismatch_index(sl_dict, k)
666
+ random_kmer = extract_kmers(random_sequences_dict, k)
667
+ random_mismatch_to_kmer = build_mismatch_index(random_sequences_dict, k)
668
+
669
+ length_scores = {}
670
+
671
+ SL_ref_length = get_sequences_by_length(sl_dict)
672
+ for SL, info in SL_ref_length.items():
673
+ length_score = length_index(SL, info['sequence'], kmer, mismatch_to_kmer, random_seq_len, k)
674
+ length_scores[len(info['sequence'])] = length_score
675
+
676
+ bam_file = pysam.AlignmentFile(args.input, 'rb')
677
+ timestamp = int(time.time())
678
+ tmp_output_name = f"tmp_{timestamp}.csv"
679
+ outfile = open(tmp_output_name, "w")
680
+ outfile.write("query_name\tstrand\tsoft_length\taligned_length\tread_end\tquery_length\tconsensus\trandom_sw_score\trandom_final_score\trandom_SL_score\tsw_score\tfinal_score\tSL_score\tSL_type\n")
681
+
682
+ # 迭代每个read
683
+ print('Loading the BAM file')
684
+ bam_list=[]
685
+ for read in bam_file.fetch():
686
+ if read.is_supplementary or read.is_secondary:
687
+ continue
688
+ ### sequence extract
689
+ query_name = read.query_name # 获取query的名称
690
+ full_query_sequence = read.query_sequence # 获取query的序列
691
+ if read.is_reverse:
692
+ strand = '-'
693
+ else:
694
+ strand = '+'
695
+ bam_list.append([query_name,full_query_sequence,strand,read.cigartuples,read.query_alignment_length])
696
+ pbar = tqdm(total=len(bam_list), position=0, leave=True)
697
+
698
+ if mode == 'RNA':
699
+ with multiprocessing.Pool(processes=args.cpu) as pool:
700
+ for item in bam_list:
701
+ pool.apply_async(drs_calculation_per_process, args=(item, sl_dict, length_scores, random_sequences_dict,
702
+ random_seq_len, random_kmer, random_mismatch_to_kmer,
703
+ k, kmer, mismatch_to_kmer), callback=update)
704
+ pool.close()
705
+ pool.join()
706
+ elif mode == 'cDNA':
707
+ with multiprocessing.Pool(processes=args.cpu) as pool:
708
+ for item in bam_list:
709
+ pool.apply_async(cdna_calculation_per_process, args=(item, sl_dict, length_scores, random_sequences_dict,
710
+ random_seq_len, random_kmer,
711
+ random_mismatch_to_kmer,
712
+ k, kmer, mismatch_to_kmer), callback=update)
713
+ pool.close()
714
+ pool.join()
715
+
716
+ pbar.close()
717
+ outfile.close()
718
+ df = pd.read_csv(tmp_output_name,sep='\t')
719
+ df.sort_values(by=['query_name'], inplace=True)
720
+ df.to_csv(args.output, index=False,sep='\t')
721
+ if args.visualization:
722
+ visualize_html(args.output, args.cutoff)
723
+ print('Finished')
724
+ print('Finished')
725
+
726
+ if __name__ == '__main__':
727
+ parser = argparse.ArgumentParser(
728
+ description="help to know spliced leader and distinguish SL1 and SL2")
729
+ parser.add_argument("-r", "--refer", type=str,required=True,
730
+ help="SL reference")
731
+ parser.add_argument("-i", "--input", type=str, metavar="",required=True,
732
+ help="input the bam file")
733
+ parser.add_argument("-m", "--mode", type=str, metavar="", choices=['RNA','cDNA'],
734
+ default="RNA", help="RNA or cDNA")
735
+ parser.add_argument("-o", "--output", type=str, metavar="",
736
+ default="SLRanger_ppssw.txt",
737
+ help="output file")
738
+ parser.add_argument("-c", "--cutoff", type=float, default=4, help="cutoff of high confident SL sequence")
739
+ parser.add_argument("--visualization", action='store_true', help='Turn on the visualization mode')
740
+ parser.add_argument("-t", "--cpu", type=int,
741
+ default=1,
742
+ help="number if CPU")
743
+ args = parser.parse_args()
744
+ main(args)