1use crate::annotation::Annotation;
9use crate::is_bsj_hg2::{java_substring, report_scan1_hg_profile, IsBSJHg2};
10use crate::misd::misd;
11use crate::runtime::{emit_perf_line, emit_trace_line, scan1_profile_enabled, should_trace_read};
12use crate::utils::{
13 alignment_short_cs, bam_shard_count, clip_sequence_payload, local_clip_evidence_lines,
14 part_path, AlignmentRecord,
15};
16use anyhow::Result;
17use indicatif::{ProgressBar, ProgressStyle};
18use memmap2::Mmap;
19use noodles::sam::{
20 self,
21 alignment::{
22 record::{
23 data::field::{Tag, Value},
24 Data as _, Sequence as _,
25 },
26 Record as _,
27 },
28};
29use rayon::prelude::*;
30use std::borrow::Cow;
31use std::collections::{HashMap, HashSet};
32use std::fmt::Write as FmtWrite;
33use std::fs::File;
34use std::io::{BufRead, BufReader, BufWriter, Seek, Write};
35use std::sync::atomic::{AtomicU64, Ordering};
36use std::sync::mpsc;
37use std::thread;
38use std::time::{Duration, Instant};
39
40pub struct Scan1 {
49 pub min_mapq_uni: i32,
50 pub max_circle: i32,
51 pub min_circle: i32,
52 pub linear_range_size_min: i32,
53 pub mem_limit: u64,
54 pub read_len: i32,
55 pub mapped_reads: u64,
57 pub bsj1_reads: usize,
59}
60
61type OwnedAlignmentRecord = AlignmentRecord<'static>;
62type OwnedStandMap = HashMap<i32, (char, Cow<'static, str>)>;
63
64fn xa_payload(raw: &str) -> &str {
70 if raw.is_empty() {
71 "*"
72 } else {
73 raw
74 }
75}
76
77fn raw_xa_from_sam_record(record: &sam::Record) -> Result<String> {
83 let tag = Tag::new(b'X', b'A');
84 let data = record.data();
85 let Some(value) = data.get(&tag).transpose()? else {
86 return Ok(String::new());
87 };
88 let Value::String(raw) = value else {
89 return Ok(String::new());
90 };
91 let Ok(raw) = std::str::from_utf8(raw.as_ref()) else {
92 return Ok(String::new());
93 };
94 Ok(raw.to_string())
95}
96
97fn raw_xa_from_bam_record(record: &noodles::bam::Record) -> Result<String> {
102 let tag = Tag::new(b'X', b'A');
103 let data = record.data();
104 let Some(value) = data.get(&tag).transpose()? else {
105 return Ok(String::new());
106 };
107 let Value::String(raw) = value else {
108 return Ok(String::new());
109 };
110 let Ok(raw) = std::str::from_utf8(raw.as_ref()) else {
111 return Ok(String::new());
112 };
113 Ok(raw.to_string())
114}
115
116struct SamOwnedGroup {
122 read_id: String,
123 group: [Vec<OwnedAlignmentRecord>; 2],
124 stand_map: OwnedStandMap,
125 align_num: usize,
126}
127
128struct Scan1TraversalStats {
130 max_read_len: i32,
131 mapped_reads: u64,
132}
133
134struct Scan1ShardStats {
136 max_read_len: i32,
137 mapped_reads: u64,
138}
139
140fn advise_dontneed_aligned(mmap: &Mmap, offset: usize, len: usize) {
145 if len == 0 {
146 return;
147 }
148 let page_size = 4096;
150 let aligned_offset = (offset / page_size) * page_size;
151 let aligned_len = ((offset + len + page_size - 1) / page_size) * page_size - aligned_offset;
152
153 unsafe {
154 let ptr = mmap.as_ptr().add(aligned_offset);
155 libc::madvise(ptr as *mut libc::c_void, aligned_len, libc::MADV_DONTNEED);
156 }
157}
158
159fn trace_bam_shard_event(
166 shard_idx: usize,
167 read_id: &str,
168 stage: &str,
169 start: usize,
170 block_start: usize,
171 end: usize,
172 abs_c_pos: usize,
173) {
174 if should_trace_read(read_id) {
175 emit_trace_line(&format!(
176 "[TRACE_SCAN1_SHARD] shard={} stage={} id={} start={} block_start={} end={} abs_c_pos={}",
177 shard_idx, stage, read_id, start, block_start, end, abs_c_pos
178 ));
179 }
180}
181
182#[inline]
191fn update_best_stand_seq<'a>(
192 stand_map: &mut HashMap<i32, (char, Cow<'a, str>)>,
193 s_idx: i32,
194 st_c: char,
195 seq: Cow<'a, str>,
196) {
197 match stand_map.get(&s_idx) {
198 Some((_, existing_seq)) if existing_seq.len() >= seq.len() => {}
199 _ => {
200 stand_map.insert(s_idx, (st_c, seq));
201 }
202 }
203}
204
205#[derive(Default)]
212struct Scan1Profile {
213 wall_total_ns: AtomicU64,
214 shard_total_ns: AtomicU64,
215 group_process_ns: AtomicU64,
216 bsj_judge_ns: AtomicU64,
217 write_ns: AtomicU64,
218 merge_ns: AtomicU64,
219 records: AtomicU64,
220 groups: AtomicU64,
221 hg1_calls: AtomicU64,
222 hg1_hits: AtomicU64,
223}
224
225impl Scan1Profile {
226 fn enabled_from_env() -> bool {
228 scan1_profile_enabled()
229 }
230
231 fn report(&self) {
236 let wall_total_ns = self.wall_total_ns.load(Ordering::Relaxed);
237 let shard_total_ns = self.shard_total_ns.load(Ordering::Relaxed);
238 let group_process_ns = self.group_process_ns.load(Ordering::Relaxed);
239 let bsj_judge_ns = self.bsj_judge_ns.load(Ordering::Relaxed);
240 let write_ns = self.write_ns.load(Ordering::Relaxed);
241 let merge_ns = self.merge_ns.load(Ordering::Relaxed);
242 let records = self.records.load(Ordering::Relaxed);
243 let groups = self.groups.load(Ordering::Relaxed);
244 let hg1_calls = self.hg1_calls.load(Ordering::Relaxed);
245 let hg1_hits = self.hg1_hits.load(Ordering::Relaxed);
246 let other_shard_ns =
247 shard_total_ns.saturating_sub(group_process_ns.saturating_add(write_ns));
248 let pct = |part: u64, whole: u64| -> f64 {
249 if whole == 0 {
250 0.0
251 } else {
252 part as f64 * 100.0 / whole as f64
253 }
254 };
255 emit_perf_line(&format!(
256 "[PROFILE_SCAN1] wall_ms={:.3} shard_work_ms={:.3} merge_ms={:.3} records={} groups={} hg1_calls={} hg1_hits={}",
257 wall_total_ns as f64 / 1_000_000.0,
258 shard_total_ns as f64 / 1_000_000.0,
259 merge_ns as f64 / 1_000_000.0,
260 records,
261 groups,
262 hg1_calls,
263 hg1_hits
264 ));
265 emit_perf_line(&format!(
266 "[PROFILE_SCAN1] shard_breakdown_ms group_process={:.3} ({:.1}%) bsj_judge={:.3} ({:.1}% of group) write={:.3} ({:.1}%) other={:.3} ({:.1}%)",
267 group_process_ns as f64 / 1_000_000.0,
268 pct(group_process_ns, shard_total_ns),
269 bsj_judge_ns as f64 / 1_000_000.0,
270 pct(bsj_judge_ns, group_process_ns),
271 write_ns as f64 / 1_000_000.0,
272 pct(write_ns, shard_total_ns),
273 other_shard_ns as f64 / 1_000_000.0,
274 pct(other_shard_ns, shard_total_ns),
275 ));
276 report_scan1_hg_profile();
277 }
278}
279
280impl Scan1 {
281 #[inline]
290 fn normalize_bsj_cigar(cigar: &str) -> std::borrow::Cow<'_, str> {
291 if cigar.contains('H') {
292 std::borrow::Cow::Owned(cigar.replace('H', "S"))
293 } else {
294 std::borrow::Cow::Borrowed(cigar)
295 }
296 }
297
298 pub fn new(
300 min_mapq_uni: i32,
301 min_circle: i32,
302 max_circle: i32,
303 linear_range_size_min: i32,
304 ) -> Self {
305 Self {
306 min_mapq_uni,
307 max_circle,
308 min_circle,
309 linear_range_size_min,
310 mem_limit: 2 * 1024 * 1024 * 1024,
311 read_len: 0,
312 mapped_reads: 0,
313 bsj1_reads: 0,
314 }
315 }
316
317 pub fn set_mem_limit(&mut self, limit: u64) {
319 self.mem_limit = limit;
320 }
321
322 pub fn run(
324 &mut self,
325 sam_file: &str,
326 bsj_path: &str,
327 fasta_map: &HashMap<String, String>,
328 annotation: &Annotation,
329 ) -> Result<HashSet<String>> {
330 self.run_with_display(sam_file, bsj_path, None, fasta_map, annotation)
331 }
332
333 pub fn run_with_display(
336 &mut self,
337 sam_file: &str,
338 bsj_path: &str,
339 display_path: Option<&str>,
340 fasta_map: &HashMap<String, String>,
341 annotation: &Annotation,
342 ) -> Result<HashSet<String>> {
343 use crate::sam_bam::{detect_format, InputFormat};
344 let format = detect_format(sam_file)?;
345 match format {
346 InputFormat::Sam => {
347 self.run_sam_with_display(sam_file, bsj_path, display_path, fasta_map, annotation)
348 }
349 InputFormat::Bam => {
350 self.run_bam_with_display(sam_file, bsj_path, display_path, fasta_map, annotation)
351 }
352 }
353 }
354
355 pub fn run_with_priority(
361 &mut self,
362 sam_file: &str,
363 bsj_path: &str,
364 fasta_map: &HashMap<String, String>,
365 annotation: &Annotation,
366 ) -> Result<HashSet<String>> {
367 self.run_with_priority_and_segments(sam_file, bsj_path, None, fasta_map, annotation)
368 }
369
370 pub fn run_with_priority_and_segments(
377 &mut self,
378 sam_file: &str,
379 bsj_path: &str,
380 segments_path: Option<&str>,
381 fasta_map: &HashMap<String, String>,
382 annotation: &Annotation,
383 ) -> Result<HashSet<String>> {
384 use crate::sam_bam::{detect_format, InputFormat};
385 let format = detect_format(sam_file)?;
386 match format {
387 InputFormat::Sam => {
388 self.run_sam_with_priority(sam_file, bsj_path, segments_path, fasta_map, annotation)
389 }
390 InputFormat::Bam => {
391 self.run_bam_with_priority(sam_file, bsj_path, segments_path, fasta_map, annotation)
392 }
393 }
394 }
395
396 pub fn run_sam(
402 &mut self,
403 sam_file: &str,
404 bsj_path: &str,
405 fasta_map: &HashMap<String, String>,
406 annotation: &Annotation,
407 ) -> Result<HashSet<String>> {
408 self.run_sam_with_display(sam_file, bsj_path, None, fasta_map, annotation)
409 }
410
411 fn run_sam_with_display(
413 &mut self,
414 sam_file: &str,
415 bsj_path: &str,
416 display_path: Option<&str>,
417 fasta_map: &HashMap<String, String>,
418 annotation: &Annotation,
419 ) -> Result<HashSet<String>> {
420 let file_size = std::fs::metadata(sam_file)?.len();
421 let pb = ProgressBar::new(file_size);
422 pb.set_style(
423 ProgressStyle::default_bar()
424 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {percent:>3}% ({eta}) {msg}")?
425 .progress_chars("#>-"),
426 );
427 pb.set_message("");
428 pb.enable_steady_tick(Duration::from_millis(120));
429
430 let shard_out = part_path(bsj_path, 0);
431 let display_shard_out = display_path.map(|path| part_path(path, 0));
432 let stats = self.process_sam_file_to_file(
433 sam_file,
434 fasta_map,
435 annotation,
436 &pb,
437 &shard_out,
438 display_shard_out.as_deref(),
439 None,
440 false,
441 )?;
442 self.read_len = stats.max_read_len;
443 self.mapped_reads = stats.mapped_reads;
444
445 pb.set_style(
446 ProgressStyle::default_bar()
447 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {msg}")?
448 .progress_chars("#>-"),
449 );
450 pb.finish_with_message("");
451 let scan1_ids = self.merge_and_collect_ids(bsj_path, 1)?;
452 if let Some(path) = display_path {
453 let _ = self.merge_display_shards(path, 1)?;
454 }
455 self.bsj1_reads = scan1_ids.len();
456 Ok(scan1_ids)
457 }
458
459 fn run_sam_with_priority(
462 &mut self,
463 sam_file: &str,
464 bsj_path: &str,
465 segments_path: Option<&str>,
466 fasta_map: &HashMap<String, String>,
467 annotation: &Annotation,
468 ) -> Result<HashSet<String>> {
469 let file_size = std::fs::metadata(sam_file)?.len();
470 let pb = ProgressBar::new(file_size);
471 pb.set_style(
472 ProgressStyle::default_bar()
473 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {percent:>3}% ({eta}) {msg}")?
474 .progress_chars("#>-"),
475 );
476 pb.set_message("");
477 pb.enable_steady_tick(Duration::from_millis(120));
478
479 let shard_out = part_path(bsj_path, 0);
480 let segments_shard_out = segments_path.map(|path| part_path(path, 0));
481 let stats = self.process_sam_file_to_file(
482 sam_file,
483 fasta_map,
484 annotation,
485 &pb,
486 &shard_out,
487 None,
488 segments_shard_out.as_deref(),
489 true,
490 )?;
491 self.read_len = stats.max_read_len;
492 self.mapped_reads = stats.mapped_reads;
493
494 pb.set_style(
495 ProgressStyle::default_bar()
496 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {msg}")?
497 .progress_chars("#>-"),
498 );
499 pb.finish_with_message("");
500 let scan1_ids = self.merge_and_collect_ids(bsj_path, 1)?;
501 if let Some(path) = segments_path {
502 self.merge_segment_shards(path, 1)?;
503 }
504 self.bsj1_reads = scan1_ids.len();
505 Ok(scan1_ids)
506 }
507
508 fn process_sam_file_to_file(
514 &self,
515 sam_file: &str,
516 fasta_map: &HashMap<String, String>,
517 annotation: &Annotation,
518 pb: &ProgressBar,
519 out_path: &str,
520 display_out_path: Option<&str>,
521 segments_out_path: Option<&str>,
522 priority_inline: bool,
523 ) -> Result<Scan1TraversalStats> {
524 let mut writer = BufWriter::with_capacity(256 * 1024, File::create(out_path)?);
525 let mut display_writer = if let Some(path) = display_out_path {
526 Some(BufWriter::with_capacity(256 * 1024, File::create(path)?))
527 } else {
528 None
529 };
530 let mut segments_writer = if let Some(path) = segments_out_path {
531 Some(BufWriter::with_capacity(256 * 1024, File::create(path)?))
532 } else {
533 None
534 };
535 let batch_size = (rayon::current_num_threads().max(1) * 256).max(1024);
536 let (tx, rx) =
537 mpsc::sync_channel::<Vec<SamOwnedGroup>>(rayon::current_num_threads().max(2));
538 let pb_clone = pb.clone();
539
540 let stats = thread::scope(|scope| -> Result<Scan1TraversalStats> {
541 let producer =
542 scope.spawn(|| self.stream_sam_group_batches(sam_file, &pb_clone, tx, batch_size));
543
544 for mut batch in rx {
545 self.process_sam_group_batch(
546 &mut writer,
547 display_writer.as_mut(),
548 segments_writer.as_mut(),
549 &mut batch,
550 fasta_map,
551 annotation,
552 priority_inline,
553 )?;
554 }
555
556 producer
557 .join()
558 .map_err(|_| anyhow::anyhow!("SAM group producer thread panicked"))?
559 })?;
560
561 writer.flush()?;
562 if let Some(writer) = display_writer.as_mut() {
563 writer.flush()?;
564 }
565 if let Some(writer) = segments_writer.as_mut() {
566 writer.flush()?;
567 }
568 Ok(stats)
569 }
570
571 fn stream_sam_group_batches(
576 &self,
577 sam_file: &str,
578 pb: &ProgressBar,
579 tx: mpsc::SyncSender<Vec<SamOwnedGroup>>,
580 batch_size: usize,
581 ) -> Result<Scan1TraversalStats> {
582 let sam_reader = BufReader::with_capacity(1024 * 1024, File::open(sam_file)?);
583 let mut reader = sam::io::Reader::new(sam_reader);
584 let header = reader.read_header()?;
585 let mut record = sam::Record::default();
586 let mut current_id: Vec<u8> = Vec::new();
587 let mut group: [Vec<OwnedAlignmentRecord>; 2] =
588 [Vec::with_capacity(8), Vec::with_capacity(8)];
589 let mut stand_map: OwnedStandMap = HashMap::with_capacity(4);
590 let mut align_num = 0usize;
591 let mut max_read_len = 0i32;
592 let mut cigar_buf = String::with_capacity(64);
593 let mut seq_buf = String::with_capacity(256);
594 let mut batch = Vec::with_capacity(batch_size);
595 let mut records_since_progress = 0usize;
596 let mut last_progress_pos = reader.get_mut().stream_position()?;
597 let mut mapped_reads = 0u64;
598
599 while reader.read_record(&mut record)? != 0 {
600 let read_id = record
601 .name()
602 .ok_or_else(|| anyhow::anyhow!("Missing read name"))?;
603 if read_id != current_id.as_slice() {
604 if !current_id.is_empty() {
605 self.push_sam_owned_group(
606 &mut batch,
607 ¤t_id,
608 &mut group,
609 &mut stand_map,
610 align_num,
611 );
612 mapped_reads += 1;
613 if batch.len() >= batch_size {
614 tx.send(std::mem::take(&mut batch))
615 .map_err(|_| anyhow::anyhow!("SAM group consumer dropped"))?;
616 batch = Vec::with_capacity(batch_size);
617 }
618 }
619 current_id.clear();
620 current_id.extend_from_slice(read_id);
621 align_num = 0;
622 }
623
624 let chrom = match record.reference_sequence(&header) {
625 Some(Ok((name, _))) => String::from_utf8_lossy(name).to_string(),
626 _ => "*".to_string(),
627 };
628 let flag = i32::from(u16::from(record.flags()?));
629 let start_pos = record
630 .alignment_start()
631 .transpose()?
632 .map(|p| p.get() as i32)
633 .unwrap_or(0);
634 let mapq = record
635 .mapping_quality()
636 .transpose()?
637 .map(u8::from)
638 .unwrap_or(0) as i32;
639
640 cigar_buf.clear();
641 for result in record.cigar().iter() {
642 let op = result?;
643 use noodles::sam::alignment::record::cigar::op::Kind;
644 let op_char = match op.kind() {
645 Kind::Match => 'M',
646 Kind::Insertion => 'I',
647 Kind::Deletion => 'D',
648 Kind::Skip => 'N',
649 Kind::SoftClip => 'S',
650 Kind::HardClip => 'H',
651 Kind::Pad => 'P',
652 Kind::SequenceMatch => '=',
653 Kind::SequenceMismatch => 'X',
654 };
655 let _ = write!(&mut cigar_buf, "{}{}", op.len(), op_char);
656 }
657
658 seq_buf.clear();
659 for b in record.sequence().iter() {
660 seq_buf.push(char::from(b));
661 }
662 max_read_len = max_read_len.max(seq_buf.len() as i32);
663 let seq = seq_buf.clone();
664 let s_idx = if flag & 0x40 != 0 { 1 } else { 0 };
665 if !seq.is_empty() && seq != "*" {
666 let st_c = if flag & 0x10 != 0 { '1' } else { '0' };
667 update_best_stand_seq(&mut stand_map, s_idx, st_c, Cow::Owned(seq.clone()));
668 }
669 group[s_idx as usize].push(AlignmentRecord {
670 flag,
671 chrom: Cow::Owned(chrom),
672 pos: start_pos,
673 mapq,
674 cigar: Cow::Owned(cigar_buf.clone()),
675 seq: Cow::Owned(seq),
676 xa: Cow::Owned(raw_xa_from_sam_record(&record)?),
677 });
678 align_num += 1;
679 records_since_progress += 1;
680 if records_since_progress >= 4096 {
681 let pos = reader.get_mut().stream_position()?;
682 if pos > last_progress_pos {
683 pb.inc(pos - last_progress_pos);
684 last_progress_pos = pos;
685 }
686 records_since_progress = 0;
687 }
688 }
689
690 if !current_id.is_empty() {
691 self.push_sam_owned_group(
692 &mut batch,
693 ¤t_id,
694 &mut group,
695 &mut stand_map,
696 align_num,
697 );
698 mapped_reads += 1;
699 }
700 if !batch.is_empty() {
701 tx.send(batch)
702 .map_err(|_| anyhow::anyhow!("SAM group consumer dropped"))?;
703 }
704
705 let final_pos = reader.get_mut().stream_position()?;
706 if final_pos > last_progress_pos {
707 pb.inc(final_pos - last_progress_pos);
708 }
709
710 Ok(Scan1TraversalStats {
711 max_read_len,
712 mapped_reads,
713 })
714 }
715
716 fn push_sam_owned_group(
720 &self,
721 batch: &mut Vec<SamOwnedGroup>,
722 current_id: &[u8],
723 group: &mut [Vec<OwnedAlignmentRecord>; 2],
724 stand_map: &mut OwnedStandMap,
725 align_num: usize,
726 ) {
727 batch.push(SamOwnedGroup {
728 read_id: String::from_utf8_lossy(current_id).into_owned(),
729 group: [std::mem::take(&mut group[0]), std::mem::take(&mut group[1])],
730 stand_map: std::mem::take(stand_map),
731 align_num,
732 });
733 group[0] = Vec::with_capacity(8);
734 group[1] = Vec::with_capacity(8);
735 *stand_map = HashMap::with_capacity(4);
736 }
737
738 fn process_sam_group_batch(
740 &self,
741 writer: &mut BufWriter<File>,
742 mut display_writer: Option<&mut BufWriter<File>>,
743 mut segments_writer: Option<&mut BufWriter<File>>,
744 batch: &mut Vec<SamOwnedGroup>,
745 fasta_map: &HashMap<String, String>,
746 annotation: &Annotation,
747 priority_inline: bool,
748 ) -> Result<()> {
749 let groups = std::mem::take(batch);
750 let results: Vec<(Option<(String, String)>, Vec<String>, Vec<String>)> = groups
751 .into_par_iter()
752 .map(|owned| {
753 let non_empty_groups = owned.group.iter().filter(|g| !g.is_empty()).count();
754 if owned.align_num <= 2 && non_empty_groups != 1 {
755 return (None, Vec::new(), Vec::new());
756 }
757 let mut validator = IsBSJHg2::new(self.linear_range_size_min, self.min_mapq_uni);
758 let main = self
759 .process_group_view(
760 &owned.read_id,
761 &owned.group,
762 &owned.stand_map,
763 fasta_map,
764 annotation,
765 &mut validator,
766 None,
767 )
768 .map(|(_, mate_label, res_line)| (mate_label, res_line));
769 let mut display_validator =
770 IsBSJHg2::new(self.linear_range_size_min, self.min_mapq_uni);
771 let display = if priority_inline || display_writer.is_some() {
772 self.process_group_view_display(
773 &owned.read_id,
774 &owned.group,
775 &owned.stand_map,
776 fasta_map,
777 annotation,
778 &mut display_validator,
779 )
780 } else {
781 Vec::new()
782 };
783 let has_bsj = main.is_some() || !display.is_empty();
784 let evidence = if has_bsj {
785 let mut bsj_lines = Vec::new();
786 if let Some((_, res_line)) = &main {
787 bsj_lines.push(res_line.clone());
788 }
789 bsj_lines.extend(display.iter().cloned());
790 Self::segment_evidence_lines_with_local(
791 &owned.read_id,
792 "scan1",
793 "scan1_local",
794 &owned.group,
795 &bsj_lines,
796 fasta_map,
797 )
798 } else {
799 Vec::new()
800 };
801 (main, display, evidence)
802 })
803 .collect();
804
805 for (main, display, evidence) in results {
806 if priority_inline {
807 for line in Self::prioritize_scan1_display_lines(main, display) {
808 writeln!(writer, "{}", line)?;
809 }
810 } else {
811 if let Some((_, res_line)) = main {
812 writeln!(writer, "{}", res_line)?;
813 }
814 if let Some(display_writer) = display_writer.as_deref_mut() {
815 for line in display {
816 writeln!(display_writer, "{}", line)?;
817 }
818 }
819 }
820 if let Some(segments_writer) = segments_writer.as_deref_mut() {
821 for line in evidence {
822 writeln!(segments_writer, "{}", line)?;
823 }
824 }
825 }
826
827 Ok(())
828 }
829
830 fn merge_and_collect_ids(&self, bsj_path: &str, num_threads: usize) -> Result<HashSet<String>> {
836 let mut final_writer = BufWriter::with_capacity(1024 * 1024, File::create(bsj_path)?);
837 let mut scan1_id_map = HashSet::new();
838 for i in 0..num_threads {
839 let shard_path = part_path(bsj_path, i);
840 if let Ok(shard_file) = File::open(&shard_path) {
841 let mut reader = BufReader::new(shard_file);
842 let mut line = String::new();
843 loop {
844 line.clear();
845 if reader.read_line(&mut line)? == 0 {
846 break;
847 }
848 let trimmed = line.trim_end();
849 let parts: Vec<&str> = trimmed.split('\t').collect();
850 if !parts.is_empty() && crate::utils::bsj_is_summary_priority(&parts) {
851 scan1_id_map.insert(parts[0].to_string());
852 }
853 if !trimmed.is_empty() {
854 final_writer.write_all(line.as_bytes())?;
855 }
856 }
857 }
858 let _ = std::fs::remove_file(shard_path);
859 }
860 final_writer.flush()?;
861 Ok(scan1_id_map)
862 }
863
864 fn prioritize_scan1_display_lines(
870 main: Option<(String, String)>,
871 display: Vec<String>,
872 ) -> Vec<String> {
873 let Some((main_mate, main_line)) = main else {
874 return display
875 .into_iter()
876 .filter_map(|line| Self::with_priority_from_display_line(&line, "0"))
877 .collect();
878 };
879 let Some((main_id, main_payload)) = main_line.split_once('\t') else {
880 return Vec::new();
881 };
882 let main_key = format!("{}\t{}\t{}", main_id, main_mate, main_payload);
883 let mut wrote_main = false;
884 let mut rows = Vec::new();
885 for line in display {
886 if let Some((key, priority_line)) = Self::priority_display_key_and_line(&line, "0") {
887 if key == main_key && !wrote_main {
888 if let Some(priority_line) = Self::with_priority_from_display_line(&line, "1") {
889 rows.push(priority_line);
890 }
891 wrote_main = true;
892 } else {
893 rows.push(priority_line);
894 }
895 }
896 }
897 if !wrote_main {
898 rows.push(format!("{}\t{}\t1\t{}", main_id, main_mate, main_payload));
899 }
900 rows
901 }
902
903 fn with_priority_from_display_line(line: &str, priority: &str) -> Option<String> {
906 let mut parts = line.splitn(3, '\t');
907 let read_id = parts.next()?;
908 let mate_label = parts.next()?;
909 let payload = parts.next()?;
910 Some(format!("{read_id}\t{mate_label}\t{priority}\t{payload}"))
911 }
912
913 fn priority_display_key_and_line(line: &str, priority: &str) -> Option<(String, String)> {
915 let mut parts = line.splitn(3, '\t');
916 let read_id = parts.next()?;
917 let mate_label = parts.next()?;
918 let payload = parts.next()?;
919 Some((
920 format!("{read_id}\t{mate_label}\t{payload}"),
921 format!("{read_id}\t{mate_label}\t{priority}\t{payload}"),
922 ))
923 }
924
925 pub fn run_bam(
930 &mut self,
931 bam_file: &str,
932 bsj_path: &str,
933 fasta_map: &HashMap<String, String>,
934 annotation: &Annotation,
935 ) -> Result<HashSet<String>> {
936 self.run_bam_with_display(bam_file, bsj_path, None, fasta_map, annotation)
937 }
938
939 fn run_bam_with_priority(
942 &mut self,
943 bam_file: &str,
944 bsj_path: &str,
945 segments_path: Option<&str>,
946 fasta_map: &HashMap<String, String>,
947 annotation: &Annotation,
948 ) -> Result<HashSet<String>> {
949 self.run_bam_with_display_impl(
950 bam_file,
951 bsj_path,
952 None,
953 segments_path,
954 fasta_map,
955 annotation,
956 true,
957 )
958 }
959
960 fn run_bam_with_display(
962 &mut self,
963 bam_file: &str,
964 bsj_path: &str,
965 display_path: Option<&str>,
966 fasta_map: &HashMap<String, String>,
967 annotation: &Annotation,
968 ) -> Result<HashSet<String>> {
969 self.run_bam_with_display_impl(
970 bam_file,
971 bsj_path,
972 display_path,
973 None,
974 fasta_map,
975 annotation,
976 false,
977 )
978 }
979
980 fn run_bam_with_display_impl(
981 &mut self,
982 bam_file: &str,
983 bsj_path: &str,
984 display_path: Option<&str>,
985 segments_path: Option<&str>,
986 fasta_map: &HashMap<String, String>,
987 annotation: &Annotation,
988 priority_inline: bool,
989 ) -> Result<HashSet<String>> {
990 use noodles::bam;
991 let run_started = Instant::now();
992 let profile = if Scan1Profile::enabled_from_env() {
993 Some(Scan1Profile::default())
994 } else {
995 None
996 };
997 let profile_ref = profile.as_ref();
998 let file = File::open(bam_file)?;
999 let file_size = std::fs::metadata(bam_file)?.len();
1000 let mmap = unsafe { Mmap::map(&file)? };
1001 let num_threads = bam_shard_count(mmap.len(), rayon::current_num_threads());
1002 let shard_size = mmap.len() / num_threads;
1003 let mut reader = bam::io::Reader::new(file);
1004 let header = reader.read_header()?;
1005
1006 let pb = ProgressBar::new(file_size);
1007 pb.set_style(
1008 ProgressStyle::default_bar()
1009 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {percent:>3}% ({eta}) {msg}")?
1010 .progress_chars("#>-"),
1011 );
1012 pb.set_message("");
1013 pb.enable_steady_tick(Duration::from_millis(120));
1014
1015 unsafe {
1016 libc::madvise(
1017 mmap.as_ptr() as *mut libc::c_void,
1018 mmap.len(),
1019 libc::MADV_SEQUENTIAL,
1020 );
1021 }
1022
1023 let header_ref = &header;
1024 let shard_stats: Vec<Scan1ShardStats> = (0..num_threads)
1025 .into_par_iter()
1026 .map(|i| {
1027 let start = i * shard_size;
1028 let end = if i == num_threads - 1 {
1029 mmap.len()
1030 } else {
1031 (i + 1) * shard_size
1032 };
1033 let shard_out = part_path(bsj_path, i);
1034 let display_shard_out = display_path.map(|path| part_path(path, i));
1035 let segments_shard_out = segments_path.map(|path| part_path(path, i));
1036 self.process_bam_shard_to_file(
1037 i,
1038 &mmap,
1039 start,
1040 end,
1041 header_ref,
1042 fasta_map,
1043 annotation,
1044 &pb,
1045 &shard_out,
1046 display_shard_out.as_deref(),
1047 segments_shard_out.as_deref(),
1048 priority_inline,
1049 profile_ref,
1050 )
1051 })
1052 .collect::<Result<Vec<_>>>()?;
1053
1054 self.read_len = shard_stats
1055 .iter()
1056 .map(|s| s.max_read_len)
1057 .max()
1058 .unwrap_or(0);
1059 self.mapped_reads = shard_stats.iter().map(|s| s.mapped_reads).sum();
1060 pb.set_position(file_size);
1061 pb.set_style(
1062 ProgressStyle::default_bar()
1063 .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {msg}")?
1064 .progress_chars("#>-"),
1065 );
1066 pb.finish_with_message("Completed");
1067 let merge_started = Instant::now();
1068 let result = self.merge_and_collect_ids(bsj_path, num_threads);
1069 if let Some(profile) = profile_ref {
1070 profile
1071 .merge_ns
1072 .fetch_add(merge_started.elapsed().as_nanos() as u64, Ordering::Relaxed);
1073 profile
1074 .wall_total_ns
1075 .fetch_add(run_started.elapsed().as_nanos() as u64, Ordering::Relaxed);
1076 profile.report();
1077 }
1078 let scan1_ids = result?;
1079 if let Some(path) = display_path {
1080 let _ = self.merge_display_shards(path, num_threads)?;
1081 }
1082 if let Some(path) = segments_path {
1083 self.merge_segment_shards(path, num_threads)?;
1084 }
1085 self.bsj1_reads = scan1_ids.len();
1086 Ok(scan1_ids)
1087 }
1088
1089 fn parse_display_claim(line: &str) -> Option<(&str, &str)> {
1091 let mut parts = line.splitn(3, '\t');
1092 match (parts.next(), parts.next(), parts.next()) {
1093 (Some(read_id), Some(mate), Some(_)) => Some((read_id, mate)),
1094 _ => None,
1095 }
1096 }
1097
1098 fn merge_display_shards(
1100 &self,
1101 output_path: &str,
1102 num_threads: usize,
1103 ) -> Result<HashSet<String>> {
1104 let mut writer = BufWriter::with_capacity(1024 * 1024, File::create(output_path)?);
1105 let mut claims = HashSet::new();
1106 for i in 0..num_threads {
1107 let shard_path = part_path(output_path, i);
1108 if let Ok(shard_file) = File::open(&shard_path) {
1109 let mut reader = BufReader::new(shard_file);
1110 let mut line = String::new();
1111 loop {
1112 line.clear();
1113 if reader.read_line(&mut line)? == 0 {
1114 break;
1115 }
1116 if let Some((rid, mate)) = Self::parse_display_claim(line.trim_end()) {
1117 claims.insert(format!("{rid}\t{mate}"));
1118 }
1119 writer.write_all(line.as_bytes())?;
1120 }
1121 }
1122 let _ = std::fs::remove_file(shard_path);
1123 }
1124 writer.flush()?;
1125 Ok(claims)
1126 }
1127
1128 fn merge_segment_shards(&self, output_path: &str, num_threads: usize) -> Result<()> {
1130 let mut writer = BufWriter::with_capacity(1024 * 1024, File::create(output_path)?);
1131 for i in 0..num_threads {
1132 let shard_path = part_path(output_path, i);
1133 if let Ok(shard_file) = File::open(&shard_path) {
1134 let mut reader = BufReader::new(shard_file);
1135 let mut line = String::new();
1136 while reader.read_line(&mut line)? != 0 {
1137 writer.write_all(line.as_bytes())?;
1138 line.clear();
1139 }
1140 }
1141 let _ = std::fs::remove_file(shard_path);
1142 }
1143 writer.flush()?;
1144 Ok(())
1145 }
1146
1147 fn segment_evidence_lines<'a>(
1153 read_id: &str,
1154 stage: &str,
1155 group: &[Vec<AlignmentRecord<'a>>; 2],
1156 reference: &HashMap<String, String>,
1157 ) -> Vec<String> {
1158 let mut rows = Vec::new();
1159 for bucket in group {
1160 for aln in bucket {
1161 if aln.chrom.as_ref() == "*" || aln.cigar.as_ref() == "*" {
1162 continue;
1163 }
1164 let mate = if aln.flag & 0x40 != 0 { "R1" } else { "R2" };
1165 let clips = clip_sequence_payload(aln.cigar.as_ref(), aln.seq.as_ref());
1166 let cs = alignment_short_cs(
1167 aln.cigar.as_ref(),
1168 aln.seq.as_ref(),
1169 aln.chrom.as_ref(),
1170 aln.pos,
1171 reference,
1172 );
1173 rows.push(format!(
1174 "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
1175 read_id,
1176 stage,
1177 mate,
1178 aln.flag,
1179 aln.chrom,
1180 aln.pos,
1181 aln.mapq,
1182 aln.cigar,
1183 aln.seq.len(),
1184 clips,
1185 cs,
1186 xa_payload(aln.xa.as_ref())
1187 ));
1188 }
1189 }
1190 rows
1191 }
1192
1193 fn segment_evidence_lines_with_local<'a>(
1195 read_id: &str,
1196 stage: &str,
1197 local_stage: &str,
1198 group: &[Vec<AlignmentRecord<'a>>; 2],
1199 bsj_lines: &[String],
1200 fasta_map: &HashMap<String, String>,
1201 ) -> Vec<String> {
1202 let mut rows = Self::segment_evidence_lines(read_id, stage, group, fasta_map);
1203 let alignments: Vec<&AlignmentRecord<'_>> =
1204 group.iter().flat_map(|bucket| bucket.iter()).collect();
1205 rows.extend(local_clip_evidence_lines(
1206 read_id,
1207 local_stage,
1208 &alignments,
1209 bsj_lines,
1210 fasta_map,
1211 10,
1212 ));
1213 rows
1214 }
1215
1216 fn process_bam_shard_to_file(
1222 &self,
1223 shard_idx: usize,
1224 mmap: &Mmap,
1225 start: usize,
1226 end: usize,
1227 header: &sam::Header,
1228 fasta_map: &HashMap<String, String>,
1229 annotation: &Annotation,
1230 pb: &ProgressBar,
1231 out_path: &str,
1232 display_out_path: Option<&str>,
1233 segments_out_path: Option<&str>,
1234 priority_inline: bool,
1235 profile: Option<&Scan1Profile>,
1236 ) -> Result<Scan1ShardStats> {
1237 use noodles::bam;
1238 let shard_started = profile.map(|_| Instant::now());
1239 let mut writer = BufWriter::with_capacity(256 * 1024, File::create(out_path)?);
1240 let mut display_writer = if let Some(path) = display_out_path {
1241 Some(BufWriter::with_capacity(256 * 1024, File::create(path)?))
1242 } else {
1243 None
1244 };
1245 let mut segments_writer = if let Some(path) = segments_out_path {
1246 Some(BufWriter::with_capacity(256 * 1024, File::create(path)?))
1247 } else {
1248 None
1249 };
1250 let mut validator = IsBSJHg2::new(self.linear_range_size_min, self.min_mapq_uni);
1251 let mut display_validator = IsBSJHg2::new(self.linear_range_size_min, self.min_mapq_uni);
1252 let block_start = if start == 0 {
1253 0
1254 } else {
1255 let mut found = None;
1256 for i in start..mmap.len().saturating_sub(3) {
1257 if &mmap[i..i + 4] == b"\x1f\x8b\x08\x04" {
1258 found = Some(i);
1259 break;
1260 }
1261 }
1262 found.unwrap_or(mmap.len())
1263 };
1264 let pos = if start == 0 {
1265 0
1266 } else {
1267 let mut found = None;
1271 let mut i = block_start
1272 .saturating_sub(1)
1273 .min(mmap.len().saturating_sub(4));
1274 loop {
1275 if i + 3 < mmap.len() && &mmap[i..i + 4] == b"\x1f\x8b\x08\x04" {
1276 found = Some(i);
1277 break;
1278 }
1279 if i == 0 {
1280 break;
1281 }
1282 i -= 1;
1283 }
1284 found.unwrap_or(0)
1285 };
1286 if pos >= mmap.len() {
1287 return Ok(Scan1ShardStats {
1288 max_read_len: 0,
1289 mapped_reads: 0,
1290 });
1291 }
1292 let mut reader = bam::io::Reader::new(&mmap[pos..]);
1293 if start == 0 {
1294 let _ = reader.read_header()?;
1295 }
1296 let mut record = bam::Record::default();
1297 let mut current_id: Vec<u8> = Vec::new();
1298 let mut group: [Vec<AlignmentRecord>; 2] = [Vec::with_capacity(8), Vec::with_capacity(8)];
1299 let mut stand_map: HashMap<i32, (char, Cow<str>)> = HashMap::with_capacity(4);
1300 let mut align_num = 0usize;
1301 let mut crossed_start = start == 0;
1302 let mut leading_partial_id: Option<Vec<u8>> = None;
1303 let mut last_progress_pos = block_start.saturating_sub(pos);
1304 let mut last_evicted_pos = pos;
1305 let eviction_threshold = 64 * 1024 * 1024;
1306 let mut shard_max_read_len = 0i32;
1307 let mut mapped_reads = 0u64;
1308 let mut cigar_buf = String::with_capacity(64);
1309 let mut seq_buf = String::with_capacity(256);
1310 while reader.read_record(&mut record)? != 0 {
1311 if let Some(profile) = profile {
1312 profile.records.fetch_add(1, Ordering::Relaxed);
1313 }
1314 let curr_c_pos = reader.get_ref().virtual_position().compressed() as usize;
1315 if curr_c_pos > last_progress_pos {
1316 pb.inc((curr_c_pos - last_progress_pos) as u64);
1317 last_progress_pos = curr_c_pos;
1318 }
1319 if curr_c_pos - (last_evicted_pos - pos) > eviction_threshold {
1320 advise_dontneed_aligned(
1321 mmap,
1322 last_evicted_pos,
1323 curr_c_pos - (last_evicted_pos - pos),
1324 );
1325 last_evicted_pos = pos + curr_c_pos;
1326 }
1327 let read_id = record
1328 .name()
1329 .ok_or_else(|| anyhow::anyhow!("Missing read name"))?;
1330 let abs_c_pos = pos + curr_c_pos;
1331 let trace_read_id = String::from_utf8_lossy(read_id);
1332 if !crossed_start {
1333 if abs_c_pos < block_start {
1334 trace_bam_shard_event(
1335 shard_idx,
1336 &trace_read_id,
1337 "before_block_start",
1338 start,
1339 block_start,
1340 end,
1341 abs_c_pos,
1342 );
1343 leading_partial_id = Some(read_id.to_vec());
1344 continue;
1345 }
1346 crossed_start = true;
1347 }
1348 if let Some(partial_id) = &leading_partial_id {
1349 if read_id == partial_id.as_slice() {
1350 trace_bam_shard_event(
1355 shard_idx,
1356 &trace_read_id,
1357 "skip_leading_partial_group",
1358 start,
1359 block_start,
1360 end,
1361 abs_c_pos,
1362 );
1363 continue;
1364 }
1365 trace_bam_shard_event(
1366 shard_idx,
1367 &trace_read_id,
1368 "leading_partial_cleared",
1369 start,
1370 block_start,
1371 end,
1372 abs_c_pos,
1373 );
1374 leading_partial_id = None;
1375 }
1376 if read_id != current_id.as_slice() {
1377 trace_bam_shard_event(
1378 shard_idx,
1379 &trace_read_id,
1380 "group_boundary",
1381 start,
1382 block_start,
1383 end,
1384 abs_c_pos,
1385 );
1386 if !current_id.is_empty() {
1387 mapped_reads += 1;
1388 let id_str = String::from_utf8_lossy(¤t_id);
1389 let non_empty_groups = group.iter().filter(|g| !g.is_empty()).count();
1390 if align_num > 2 || non_empty_groups == 1 {
1391 let main = self.process_group_view(
1392 &id_str,
1393 &group,
1394 &stand_map,
1395 fasta_map,
1396 annotation,
1397 &mut validator,
1398 profile,
1399 );
1400 if let Some((_, _, res_line)) = &main {
1401 trace_bam_shard_event(
1402 shard_idx,
1403 &id_str,
1404 "write_bsj1",
1405 start,
1406 block_start,
1407 end,
1408 abs_c_pos,
1409 );
1410 let write_started = profile.map(|_| Instant::now());
1411 if !priority_inline {
1412 writeln!(writer, "{}", res_line)?;
1413 }
1414 if let (Some(profile), Some(write_started)) = (profile, write_started) {
1415 profile.write_ns.fetch_add(
1416 write_started.elapsed().as_nanos() as u64,
1417 Ordering::Relaxed,
1418 );
1419 }
1420 }
1421 if priority_inline || display_writer.is_some() {
1422 let display_lines = self.process_group_view_display(
1423 &id_str,
1424 &group,
1425 &stand_map,
1426 fasta_map,
1427 annotation,
1428 &mut display_validator,
1429 );
1430 let write_segments_evidence =
1431 main.is_some() || !display_lines.is_empty();
1432 if let Some(segments_writer) = segments_writer.as_mut() {
1433 if write_segments_evidence {
1434 let mut bsj_lines = Vec::new();
1435 if let Some((_, _, res_line)) = &main {
1436 bsj_lines.push(res_line.clone());
1437 }
1438 bsj_lines.extend(display_lines.iter().cloned());
1439 for line in Self::segment_evidence_lines_with_local(
1440 &id_str,
1441 "scan1",
1442 "scan1_local",
1443 &group,
1444 &bsj_lines,
1445 fasta_map,
1446 ) {
1447 writeln!(segments_writer, "{}", line)?;
1448 }
1449 }
1450 }
1451 if priority_inline {
1452 let main =
1453 main.map(|(_, mate_label, res_line)| (mate_label, res_line));
1454 for line in
1455 Self::prioritize_scan1_display_lines(main, display_lines)
1456 {
1457 writeln!(writer, "{}", line)?;
1458 }
1459 } else if let Some(display_writer) = display_writer.as_mut() {
1460 for line in display_lines {
1461 writeln!(display_writer, "{}", line)?;
1462 }
1463 }
1464 } else if let Some(segments_writer) = segments_writer.as_mut() {
1465 if main.is_some() {
1466 let mut bsj_lines = Vec::new();
1467 if let Some((_, _, res_line)) = &main {
1468 bsj_lines.push(res_line.clone());
1469 }
1470 for line in Self::segment_evidence_lines_with_local(
1471 &id_str,
1472 "scan1",
1473 "scan1_local",
1474 &group,
1475 &bsj_lines,
1476 fasta_map,
1477 ) {
1478 writeln!(segments_writer, "{}", line)?;
1479 }
1480 }
1481 }
1482 }
1483 if pos + curr_c_pos > end {
1484 current_id.clear();
1485 break;
1486 }
1487 }
1488 current_id.clear();
1489 current_id.extend_from_slice(read_id);
1490 group[0].clear();
1491 group[1].clear();
1492 stand_map.clear();
1493 align_num = 0;
1494 }
1495 let chrom = match record.reference_sequence(header) {
1496 Some(Ok((name, _))) => String::from_utf8_lossy(name).to_string(),
1497 _ => "*".to_string(),
1498 };
1499 let flag = i32::from(u16::from(record.flags()));
1500 let start_pos = record
1501 .alignment_start()
1502 .transpose()?
1503 .map(|p| p.get() as i32)
1504 .unwrap_or(0);
1505 let mapq = record.mapping_quality().map(u8::from).unwrap_or(0) as i32;
1506 cigar_buf.clear();
1507 for result in record.cigar().iter() {
1508 let op = result?;
1509 use noodles::sam::alignment::record::cigar::op::Kind;
1510 let op_char = match op.kind() {
1511 Kind::Match => 'M',
1512 Kind::Insertion => 'I',
1513 Kind::Deletion => 'D',
1514 Kind::Skip => 'N',
1515 Kind::SoftClip => 'S',
1516 Kind::HardClip => 'H',
1517 Kind::Pad => 'P',
1518 Kind::SequenceMatch => '=',
1519 Kind::SequenceMismatch => 'X',
1520 };
1521 let _ = write!(&mut cigar_buf, "{}{}", op.len(), op_char);
1522 }
1523 seq_buf.clear();
1524 for b in record.sequence().iter() {
1525 seq_buf.push(char::from(b));
1526 }
1527 shard_max_read_len = shard_max_read_len.max(seq_buf.len() as i32);
1528 let seq = seq_buf.clone();
1529 if !seq.is_empty() && seq != "*" {
1530 let s_idx = if flag & 0x40 != 0 { 1 } else { 0 };
1531 let st_c = if flag & 0x10 != 0 { '1' } else { '0' };
1532 update_best_stand_seq(&mut stand_map, s_idx, st_c, Cow::Owned(seq.clone()));
1533 }
1534 group[if flag & 0x40 != 0 { 1 } else { 0 }].push(AlignmentRecord {
1535 flag,
1536 chrom: Cow::Owned(chrom),
1537 pos: start_pos,
1538 mapq,
1539 cigar: Cow::Owned(cigar_buf.clone()),
1540 seq: Cow::Owned(seq),
1541 xa: Cow::Owned(raw_xa_from_bam_record(&record)?),
1542 });
1543 align_num += 1;
1544 }
1545 if !current_id.is_empty() {
1546 mapped_reads += 1;
1547 let id_str = String::from_utf8_lossy(¤t_id);
1548 let main = self.process_group_view(
1549 &id_str,
1550 &group,
1551 &stand_map,
1552 fasta_map,
1553 annotation,
1554 &mut validator,
1555 profile,
1556 );
1557 if let Some((_, _, res_line)) = &main {
1558 trace_bam_shard_event(
1559 shard_idx,
1560 &id_str,
1561 "write_bsj1_tail",
1562 start,
1563 block_start,
1564 end,
1565 pos + last_progress_pos,
1566 );
1567 let write_started = profile.map(|_| Instant::now());
1568 if !priority_inline {
1569 writeln!(writer, "{}", res_line)?;
1570 }
1571 if let (Some(profile), Some(write_started)) = (profile, write_started) {
1572 profile
1573 .write_ns
1574 .fetch_add(write_started.elapsed().as_nanos() as u64, Ordering::Relaxed);
1575 }
1576 }
1577 if priority_inline || display_writer.is_some() {
1578 let display_lines = self.process_group_view_display(
1579 &id_str,
1580 &group,
1581 &stand_map,
1582 fasta_map,
1583 annotation,
1584 &mut display_validator,
1585 );
1586 let write_segments_evidence = main.is_some() || !display_lines.is_empty();
1587 if let Some(segments_writer) = segments_writer.as_mut() {
1588 if write_segments_evidence {
1589 let mut bsj_lines = Vec::new();
1590 if let Some((_, _, res_line)) = &main {
1591 bsj_lines.push(res_line.clone());
1592 }
1593 bsj_lines.extend(display_lines.iter().cloned());
1594 for line in Self::segment_evidence_lines_with_local(
1595 &id_str,
1596 "scan1",
1597 "scan1_local",
1598 &group,
1599 &bsj_lines,
1600 fasta_map,
1601 ) {
1602 writeln!(segments_writer, "{}", line)?;
1603 }
1604 }
1605 }
1606 if priority_inline {
1607 let main = main.map(|(_, mate_label, res_line)| (mate_label, res_line));
1608 for line in Self::prioritize_scan1_display_lines(main, display_lines) {
1609 writeln!(writer, "{}", line)?;
1610 }
1611 } else if let Some(display_writer) = display_writer.as_mut() {
1612 for line in display_lines {
1613 writeln!(display_writer, "{}", line)?;
1614 }
1615 }
1616 } else if let Some(segments_writer) = segments_writer.as_mut() {
1617 if main.is_some() {
1618 let mut bsj_lines = Vec::new();
1619 if let Some((_, _, res_line)) = &main {
1620 bsj_lines.push(res_line.clone());
1621 }
1622 for line in Self::segment_evidence_lines_with_local(
1623 &id_str,
1624 "scan1",
1625 "scan1_local",
1626 &group,
1627 &bsj_lines,
1628 fasta_map,
1629 ) {
1630 writeln!(segments_writer, "{}", line)?;
1631 }
1632 }
1633 }
1634 }
1635 writer.flush()?;
1636 if let Some(display_writer) = display_writer.as_mut() {
1637 display_writer.flush()?;
1638 }
1639 if let Some(segments_writer) = segments_writer.as_mut() {
1640 segments_writer.flush()?;
1641 }
1642 if let (Some(profile), Some(shard_started)) = (profile, shard_started) {
1643 profile
1644 .shard_total_ns
1645 .fetch_add(shard_started.elapsed().as_nanos() as u64, Ordering::Relaxed);
1646 }
1647 Ok(Scan1ShardStats {
1648 max_read_len: shard_max_read_len,
1649 mapped_reads,
1650 })
1651 }
1652
1653 fn process_group_view<'a>(
1655 &self,
1656 read_id: &str,
1657 group: &[Vec<AlignmentRecord<'a>>; 2],
1658 stand_map: &HashMap<i32, (char, Cow<'a, str>)>,
1659 fasta_map: &HashMap<String, String>,
1660 annotation: &Annotation,
1661 validator: &mut IsBSJHg2,
1662 profile: Option<&Scan1Profile>,
1663 ) -> Option<(String, String, String)> {
1664 let started = profile.map(|_| Instant::now());
1665 let result = self.process_group_view_impl(
1666 read_id, group, stand_map, fasta_map, annotation, validator, profile,
1667 );
1668 if let (Some(profile), Some(started)) = (profile, started) {
1669 profile
1670 .group_process_ns
1671 .fetch_add(started.elapsed().as_nanos() as u64, Ordering::Relaxed);
1672 profile.groups.fetch_add(1, Ordering::Relaxed);
1673 }
1674 result
1675 }
1676
1677 fn process_group_view_impl<'a>(
1684 &self,
1685 read_id: &str,
1686 group: &[Vec<AlignmentRecord<'a>>; 2],
1687 stand_map: &HashMap<i32, (char, Cow<'a, str>)>,
1688 fasta_map: &HashMap<String, String>,
1689 annotation: &Annotation,
1690 validator: &mut IsBSJHg2,
1691 profile: Option<&Scan1Profile>,
1692 ) -> Option<(String, String, String)> {
1693 let trace_read = should_trace_read(read_id);
1695 let [pair1, pair2] = group;
1696 let group_count =
1697 (if !pair1.is_empty() { 1 } else { 0 }) + (if !pair2.is_empty() { 1 } else { 0 });
1698 if group_count == 0 {
1699 return None;
1700 }
1701 let is_paried = group_count - 1;
1702 for n in 0..=is_paried {
1703 let segments = if n == 0 { pair1 } else { pair2 };
1704 let mate_segments = if n == 0 { pair2 } else { pair1 };
1705 let (read_strand, read_seq) = match stand_map.get(&(n as i32)) {
1706 Some((st, seq)) => (*st, seq.as_ref()),
1707 None => continue,
1708 };
1709 if segments.len() < 2 {
1710 continue;
1711 }
1712 let seq_len = read_seq.len() as i32;
1713 let segment_misds: Vec<[i32; 4]> = segments
1716 .iter()
1717 .map(|aln| misd(&aln.cigar, seq_len))
1718 .collect();
1719 let mate_misds: Vec<[i32; 4]> = mate_segments
1720 .iter()
1721 .map(|aln| misd(&aln.cigar, seq_len))
1722 .collect();
1723 for i in 0..segments.len() {
1724 for j in i + 1..segments.len() {
1725 let (mut al1, mut al2) = (&segments[i], &segments[j]);
1726 if al1.chrom != al2.chrom {
1727 continue;
1728 }
1729 if al1.chrom.as_ref() == "chrM" {
1731 continue;
1732 }
1733 if (al1.flag & 0x10) != (al2.flag & 0x10) {
1735 continue;
1736 }
1737 if al1.mapq < self.min_mapq_uni && al2.mapq < self.min_mapq_uni {
1739 continue;
1740 }
1741 if al1.cigar.as_ref() == "*" || al2.cigar.as_ref() == "*" {
1742 continue;
1743 }
1744 let mut c1 = segment_misds[i];
1745 let mut c2 = segment_misds[j];
1746 if c1[0] > c2[0] {
1747 std::mem::swap(&mut c1, &mut c2);
1748 std::mem::swap(&mut al1, &mut al2);
1749 }
1750 let mut identified = false;
1751 let (mut s1_n, mut s2_n, mut adj1, mut adj2): (i32, i32, i32, i32) =
1752 (0, 0, 0, 0);
1753 let (mut str1, mut str2, mut str3, mut str4) =
1754 (String::new(), String::new(), String::new(), String::new());
1755 let (mut q1, mut q2, mut sum_q) = (0, 0, 0);
1756 let al1_strand = if al1.flag & 0x10 != 0 { '1' } else { '0' };
1757 let seq_oriented: Cow<'_, str> = if al1_strand == read_strand {
1761 Cow::Borrowed(read_seq)
1762 } else {
1763 Cow::Owned(crate::utils::reverse_complement(read_seq))
1764 };
1765 if c1[0] * c2[0] == -1 {
1766 let scale = c1[0] * al1.pos + c1[2] + c2[0] * al2.pos + c2[2];
1767 if scale > 0
1768 && (c1[1] - c2[1]).abs() <= 6
1769 && scale <= self.max_circle
1770 && scale >= self.min_circle
1771 {
1772 adj1 = (c1[1] * c1[0] + c2[1] * c2[0]) / 2;
1773 adj2 = (c1[1] * c1[0] + c2[1] * c2[0]) - adj1;
1774 if adj1.abs() <= 4 {
1775 identified = true;
1776 s1_n = al1.pos + adj1;
1777 s2_n = al2.pos + c2[3] - 1 - adj2;
1778 str2 = java_substring(seq_oriented.as_ref(), 0, c1[1] + adj1)
1779 .to_string();
1780 str1 = java_substring(seq_oriented.as_ref(), c1[1] + adj1, seq_len)
1781 .to_string();
1782 str3 = "*".to_string();
1783 if al1.mapq >= self.min_mapq_uni && al2.mapq >= self.min_mapq_uni {
1784 q1 = 1;
1785 q2 = 1;
1786 sum_q = 1;
1787 } else if al1.mapq >= self.min_mapq_uni {
1788 q1 = 1;
1789 } else if al2.mapq >= self.min_mapq_uni {
1790 q2 = 1;
1791 }
1792 }
1793 }
1794 } else if (c1[0] * c2[0]).abs() == 10 {
1795 if c1[0] == -1 {
1796 let scale = al2.pos + c2[3] - 1 - al1.pos;
1797 if scale > 0
1798 && (seq_len - c2[2] - c1[1]).abs() <= 6
1799 && scale <= self.max_circle
1800 && scale >= self.min_circle
1801 {
1802 adj1 = (c2[1] + c2[3] - c1[1]) / 2;
1803 adj2 = (c2[1] + c2[3] - c1[1]) - adj1;
1804 if adj1.abs() <= 4 {
1805 identified = true;
1806 s1_n = al1.pos + adj1;
1807 s2_n = al2.pos + c2[3] - 1 - adj2;
1808 str1 = java_substring(
1809 seq_oriented.as_ref(),
1810 c1[1] + adj1,
1811 seq_len,
1812 )
1813 .to_string();
1814 str2 =
1815 java_substring(seq_oriented.as_ref(), c2[1], c1[1] + adj1)
1816 .to_string();
1817 str3 =
1818 java_substring(seq_oriented.as_ref(), 0, c2[1]).to_string();
1819 if al1.mapq >= self.min_mapq_uni
1820 && al2.mapq >= self.min_mapq_uni
1821 {
1822 q1 = 1;
1823 q2 = 1;
1824 sum_q = 1;
1825 } else if al1.mapq >= self.min_mapq_uni {
1826 q1 = 1;
1827 } else if al2.mapq >= self.min_mapq_uni {
1828 q2 = 1;
1829 }
1830 }
1831 }
1832 } else {
1833 let scale = al1.pos + c1[3] - 1 - al2.pos;
1834 if scale > 0
1835 && (c1[1] - c2[1]).abs() <= 6
1836 && scale <= self.max_circle
1837 && scale >= self.min_circle
1838 {
1839 adj1 = (c1[1] - c2[1]) / 2;
1840 adj2 = (c1[1] - c2[1]) - adj1;
1841 if adj1.abs() <= 4 {
1842 identified = true;
1843 s1_n = al2.pos + adj1;
1844 s2_n = al1.pos + c1[3] - 1 - adj2;
1845 str2 = java_substring(seq_oriented.as_ref(), 0, c1[1] - adj2)
1846 .to_string();
1847 str1 = java_substring(
1848 seq_oriented.as_ref(),
1849 c1[1] - adj2,
1850 seq_len - c2[2],
1851 )
1852 .to_string();
1853 str3 = java_substring(
1854 seq_oriented.as_ref(),
1855 seq_len - c2[2],
1856 seq_len,
1857 )
1858 .to_string();
1859 if al1.mapq >= self.min_mapq_uni
1860 && al2.mapq >= self.min_mapq_uni
1861 {
1862 q1 = 1;
1863 q2 = 1;
1864 sum_q = 1;
1865 } else if al2.mapq >= self.min_mapq_uni {
1866 q1 = 1;
1867 } else if al1.mapq >= self.min_mapq_uni {
1868 q2 = 1;
1869 }
1870 }
1871 }
1872 }
1873 }
1874 if identified {
1875 let mut s4_ok = 0;
1876 if is_paried == 1 {
1877 if let Some((mate_stand, mate_seq)) = stand_map.get(&((1 - n) as i32)) {
1878 str4 = if al1_strand != *mate_stand {
1879 mate_seq.to_string()
1880 } else {
1881 crate::utils::reverse_complement(mate_seq)
1882 };
1883 }
1884 for (m_aln, mc) in mate_segments.iter().zip(mate_misds.iter()) {
1885 if m_aln.chrom == al1.chrom && m_aln.mapq >= self.min_mapq_uni {
1886 if m_aln.flag & 0x10 != al1.flag & 0x10
1887 && m_aln.pos >= s1_n - 6
1888 && m_aln.pos + mc[3] - 1 <= s2_n + 6
1889 {
1890 s4_ok = 1;
1891 break;
1892 } else {
1893 s4_ok = -1;
1894 break;
1895 }
1896 }
1897 }
1898 } else {
1899 s4_ok = 1;
1900 str4 = "".to_string();
1901 }
1902 let mut line_arr = [
1903 if al1.flag & 0x10 != 0 { "1" } else { "0" }.to_string(),
1904 al1.chrom.to_string(),
1905 str1,
1906 str2,
1907 str3,
1908 str4,
1909 q1.to_string(),
1910 q2.to_string(),
1911 s4_ok.to_string(),
1912 s1_n.to_string(),
1913 s2_n.to_string(),
1914 adj1.to_string(),
1915 adj2.to_string(),
1916 ];
1917 if trace_read {
1918 emit_trace_line(&format!(
1919 "[TRACE_SCAN1_CAND] id={} n={} al1=({}, {}, {}, {}) al2=({}, {}, {}, {}) c1={:?} c2={:?} s1_n={} s2_n={} adj1={} adj2={} q=({},{},{}) s4_ok={} line_arr6_8={}/{}/{}",
1920 read_id,
1921 n,
1922 al1.chrom,
1923 al1.pos,
1924 al1.mapq,
1925 al1.cigar,
1926 al2.chrom,
1927 al2.pos,
1928 al2.mapq,
1929 al2.cigar,
1930 c1,
1931 c2,
1932 s1_n,
1933 s2_n,
1934 adj1,
1935 adj2,
1936 q1,
1937 q2,
1938 sum_q,
1939 s4_ok,
1940 line_arr[6],
1941 line_arr[7],
1942 line_arr[8]
1943 ));
1944 }
1945 if let Some(chr_seq) = fasta_map.get(al1.chrom.as_ref()) {
1946 let hg1_started = profile.map(|_| Instant::now());
1947 let res = validator.is_bsj_hg1(
1948 &mut line_arr,
1949 chr_seq,
1950 sum_q,
1951 "chrM",
1952 false,
1953 &annotation.chr_exon_start_map,
1954 &annotation.chr_exon_end_map,
1955 );
1956 if let Some(profile) = profile {
1957 profile.hg1_calls.fetch_add(1, Ordering::Relaxed);
1958 if let Some(hg1_started) = hg1_started {
1959 profile.bsj_judge_ns.fetch_add(
1960 hg1_started.elapsed().as_nanos() as u64,
1961 Ordering::Relaxed,
1962 );
1963 }
1964 if res.is_some() {
1965 profile.hg1_hits.fetch_add(1, Ordering::Relaxed);
1966 }
1967 }
1968 if trace_read {
1969 emit_trace_line(&format!(
1970 "[TRACE_SCAN1_HG1] id={} result={} post_line_arr6_8={}/{}/{} sites={}->{}",
1971 read_id,
1972 if res.is_some() { "Some" } else { "None" },
1973 line_arr[6],
1974 line_arr[7],
1975 line_arr[8],
1976 line_arr[9],
1977 line_arr[10]
1978 ));
1979 }
1980 if let Some(res) = res {
1981 let cigar1 = Self::normalize_bsj_cigar(al1.cigar.as_ref());
1982 let cigar2 = Self::normalize_bsj_cigar(al2.cigar.as_ref());
1983 let mate_label = if n == 1 { "R1" } else { "R2" };
1984 return Some((
1985 read_id.to_string(),
1986 mate_label.to_string(),
1987 format!("{}\t{};{}\t{}", read_id, cigar1, cigar2, res),
1988 ));
1989 }
1990 }
1991 }
1992 }
1993 }
1994 }
1995 None
1996 }
1997
1998 fn process_group_view_display<'a>(
2004 &self,
2005 read_id: &str,
2006 group: &[Vec<AlignmentRecord<'a>>; 2],
2007 stand_map: &HashMap<i32, (char, Cow<'a, str>)>,
2008 fasta_map: &HashMap<String, String>,
2009 annotation: &Annotation,
2010 validator: &mut IsBSJHg2,
2011 ) -> Vec<String> {
2012 let trace_read = should_trace_read(read_id);
2013 let [pair1, pair2] = group;
2014 let group_count =
2015 (if !pair1.is_empty() { 1 } else { 0 }) + (if !pair2.is_empty() { 1 } else { 0 });
2016 if group_count == 0 {
2017 return Vec::new();
2018 }
2019 let is_paired = group_count == 2;
2020 let mut results = Vec::new();
2021
2022 'mate: for slot in [1_i32, 0_i32] {
2023 let segments = if slot == 0 { pair1 } else { pair2 };
2024 let mate_segments = if slot == 0 { pair2 } else { pair1 };
2025 let (read_strand, read_seq) = match stand_map.get(&slot) {
2026 Some((st, seq)) => (*st, seq.as_ref()),
2027 None => continue,
2028 };
2029 if segments.len() < 2 {
2030 continue;
2031 }
2032 let seq_len = read_seq.len() as i32;
2033 let segment_misds: Vec<[i32; 4]> = segments
2034 .iter()
2035 .map(|aln| misd(&aln.cigar, seq_len))
2036 .collect();
2037 let mate_misds: Vec<[i32; 4]> = mate_segments
2038 .iter()
2039 .map(|aln| misd(&aln.cigar, seq_len))
2040 .collect();
2041 for i in 0..segments.len() {
2042 for j in i + 1..segments.len() {
2043 let (mut al1, mut al2) = (&segments[i], &segments[j]);
2044 if al1.chrom != al2.chrom
2045 || al1.chrom.as_ref() == "chrM"
2046 || (al1.flag & 0x10) != (al2.flag & 0x10)
2047 || (al1.mapq < self.min_mapq_uni && al2.mapq < self.min_mapq_uni)
2048 || al1.cigar.as_ref() == "*"
2049 || al2.cigar.as_ref() == "*"
2050 {
2051 continue;
2052 }
2053 let mut c1 = segment_misds[i];
2054 let mut c2 = segment_misds[j];
2055 if c1[0] > c2[0] {
2056 std::mem::swap(&mut c1, &mut c2);
2057 std::mem::swap(&mut al1, &mut al2);
2058 }
2059 let mut identified = false;
2060 let (mut s1_n, mut s2_n, mut adj1, mut adj2): (i32, i32, i32, i32) =
2061 (0, 0, 0, 0);
2062 let (mut str1, mut str2, mut str3, mut str4) =
2063 (String::new(), String::new(), String::new(), String::new());
2064 let (mut q1, mut q2, mut sum_q) = (0, 0, 0);
2065 let al1_strand = if al1.flag & 0x10 != 0 { '1' } else { '0' };
2066 let seq_oriented: Cow<'_, str> = if al1_strand == read_strand {
2067 Cow::Borrowed(read_seq)
2068 } else {
2069 Cow::Owned(crate::utils::reverse_complement(read_seq))
2070 };
2071
2072 if c1[0] * c2[0] == -1 {
2073 let scale = c1[0] * al1.pos + c1[2] + c2[0] * al2.pos + c2[2];
2074 if scale > 0
2075 && (c1[1] - c2[1]).abs() <= 6
2076 && scale <= self.max_circle
2077 && scale >= self.min_circle
2078 {
2079 adj1 = (c1[1] * c1[0] + c2[1] * c2[0]) / 2;
2080 adj2 = (c1[1] * c1[0] + c2[1] * c2[0]) - adj1;
2081 if adj1.abs() <= 4 {
2082 identified = true;
2083 s1_n = al1.pos + adj1;
2084 s2_n = al2.pos + c2[3] - 1 - adj2;
2085 str2 = java_substring(seq_oriented.as_ref(), 0, c1[1] + adj1)
2086 .to_string();
2087 str1 = java_substring(seq_oriented.as_ref(), c1[1] + adj1, seq_len)
2088 .to_string();
2089 str3 = "*".to_string();
2090 if al1.mapq >= self.min_mapq_uni && al2.mapq >= self.min_mapq_uni {
2091 q1 = 1;
2092 q2 = 1;
2093 sum_q = 1;
2094 } else if al1.mapq >= self.min_mapq_uni {
2095 q1 = 1;
2096 } else if al2.mapq >= self.min_mapq_uni {
2097 q2 = 1;
2098 }
2099 }
2100 }
2101 } else if (c1[0] * c2[0]).abs() == 10 {
2102 if c1[0] == -1 {
2103 let scale = al2.pos + c2[3] - 1 - al1.pos;
2104 if scale > 0
2105 && (seq_len - c2[2] - c1[1]).abs() <= 6
2106 && scale <= self.max_circle
2107 && scale >= self.min_circle
2108 {
2109 adj1 = (c2[1] + c2[3] - c1[1]) / 2;
2110 adj2 = (c2[1] + c2[3] - c1[1]) - adj1;
2111 if adj1.abs() <= 4 {
2112 identified = true;
2113 s1_n = al1.pos + adj1;
2114 s2_n = al2.pos + c2[3] - 1 - adj2;
2115 str1 = java_substring(
2116 seq_oriented.as_ref(),
2117 c1[1] + adj1,
2118 seq_len,
2119 )
2120 .to_string();
2121 str2 =
2122 java_substring(seq_oriented.as_ref(), c2[1], c1[1] + adj1)
2123 .to_string();
2124 str3 =
2125 java_substring(seq_oriented.as_ref(), 0, c2[1]).to_string();
2126 if al1.mapq >= self.min_mapq_uni
2127 && al2.mapq >= self.min_mapq_uni
2128 {
2129 q1 = 1;
2130 q2 = 1;
2131 sum_q = 1;
2132 } else if al1.mapq >= self.min_mapq_uni {
2133 q1 = 1;
2134 } else if al2.mapq >= self.min_mapq_uni {
2135 q2 = 1;
2136 }
2137 }
2138 }
2139 } else {
2140 let scale = al1.pos + c1[3] - 1 - al2.pos;
2141 if scale > 0
2142 && (c1[1] - c2[1]).abs() <= 6
2143 && scale <= self.max_circle
2144 && scale >= self.min_circle
2145 {
2146 adj1 = (c1[1] - c2[1]) / 2;
2147 adj2 = (c1[1] - c2[1]) - adj1;
2148 if adj1.abs() <= 4 {
2149 identified = true;
2150 s1_n = al2.pos + adj1;
2151 s2_n = al1.pos + c1[3] - 1 - adj2;
2152 str2 = java_substring(seq_oriented.as_ref(), 0, c1[1] - adj2)
2153 .to_string();
2154 str1 = java_substring(
2155 seq_oriented.as_ref(),
2156 c1[1] - adj2,
2157 seq_len - c2[2],
2158 )
2159 .to_string();
2160 str3 = java_substring(
2161 seq_oriented.as_ref(),
2162 seq_len - c2[2],
2163 seq_len,
2164 )
2165 .to_string();
2166 if al1.mapq >= self.min_mapq_uni
2167 && al2.mapq >= self.min_mapq_uni
2168 {
2169 q1 = 1;
2170 q2 = 1;
2171 sum_q = 1;
2172 } else if al2.mapq >= self.min_mapq_uni {
2173 q1 = 1;
2174 } else if al1.mapq >= self.min_mapq_uni {
2175 q2 = 1;
2176 }
2177 }
2178 }
2179 }
2180 }
2181 if !identified {
2182 continue;
2183 }
2184
2185 let mut s4_ok = 0;
2186 if is_paired {
2187 if let Some((mate_stand, mate_seq)) = stand_map.get(&(1 - slot)) {
2188 str4 = if al1_strand != *mate_stand {
2189 mate_seq.to_string()
2190 } else {
2191 crate::utils::reverse_complement(mate_seq)
2192 };
2193 }
2194 for (m_aln, mc) in mate_segments.iter().zip(mate_misds.iter()) {
2195 if m_aln.chrom == al1.chrom && m_aln.mapq >= self.min_mapq_uni {
2196 if m_aln.flag & 0x10 != al1.flag & 0x10
2197 && m_aln.pos >= s1_n - 6
2198 && m_aln.pos + mc[3] - 1 <= s2_n + 6
2199 {
2200 s4_ok = 1;
2201 break;
2202 } else {
2203 s4_ok = -1;
2204 break;
2205 }
2206 }
2207 }
2208 } else {
2209 s4_ok = 1;
2210 str4.clear();
2211 }
2212
2213 let mut line_arr = [
2214 if al1.flag & 0x10 != 0 { "1" } else { "0" }.to_string(),
2215 al1.chrom.to_string(),
2216 str1,
2217 str2,
2218 str3,
2219 str4,
2220 q1.to_string(),
2221 q2.to_string(),
2222 s4_ok.to_string(),
2223 s1_n.to_string(),
2224 s2_n.to_string(),
2225 adj1.to_string(),
2226 adj2.to_string(),
2227 ];
2228 if trace_read {
2229 emit_trace_line(&format!(
2230 "[TRACE_SCAN1_DISPLAY] id={} slot={} al1=({}, {}, {}, {}) al2=({}, {}, {}, {}) c1={:?} c2={:?} s1_n={} s2_n={} adj1={} adj2={} q=({},{},{}) s4_ok={}",
2231 read_id,
2232 slot,
2233 al1.chrom,
2234 al1.pos,
2235 al1.mapq,
2236 al1.cigar,
2237 al2.chrom,
2238 al2.pos,
2239 al2.mapq,
2240 al2.cigar,
2241 c1,
2242 c2,
2243 s1_n,
2244 s2_n,
2245 adj1,
2246 adj2,
2247 q1,
2248 q2,
2249 sum_q,
2250 s4_ok,
2251 ));
2252 }
2253 if let Some(chr_seq) = fasta_map.get(al1.chrom.as_ref()) {
2254 let res = validator.is_bsj_hg1(
2255 &mut line_arr,
2256 chr_seq,
2257 sum_q,
2258 "chrM",
2259 false,
2260 &annotation.chr_exon_start_map,
2261 &annotation.chr_exon_end_map,
2262 );
2263 if let Some(res) = res {
2264 let cigar1 = Self::normalize_bsj_cigar(al1.cigar.as_ref());
2265 let cigar2 = Self::normalize_bsj_cigar(al2.cigar.as_ref());
2266 let mate_label = if slot == 1 { "R1" } else { "R2" };
2267 results.push(format!(
2268 "{}\t{}\t{};{}\t{}",
2269 read_id, mate_label, cigar1, cigar2, res
2270 ));
2271 continue 'mate;
2272 }
2273 }
2274 }
2275 }
2276 }
2277
2278 results
2279 }
2280}