1use bio_types::sequence::SequenceRead;
7#[cfg(feature = "pileuprayon")]
8use rayon::prelude::*;
9use std::cmp::Ordering;
10use std::collections::HashMap;
11use std::collections::hash_map::Entry::Occupied;
12use std::collections::hash_map::Entry::Vacant;
13use std::fmt;
14use std::fmt::Display;
15use std::iter;
16use std::path::Path;
17use std::slice;
18use std::thread::available_parallelism;
19
20use crate::bam::FetchDefinition;
21use crate::bam::FetchDefinition::RegionString;
22use crate::bam::Read;
23use crate::bam::ext::BamRecordExtensions;
24use crate::bam::ext::IterAlignedPairsFullCigar;
25use crate::bam::record::Cigar;
26use crate::faidx;
27use crate::htslib;
28
29use crate::bam;
30use crate::bam::record;
31use crate::errors::{Error, Result};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35#[allow(non_snake_case)]
36pub struct RustPileupConfig {
38 pub onlyprimary: bool,
39 recalculateBAM: bool,
40 pub minqual: u8,
41 pub minscore: u8,
42 pub reversedel: bool,
43 pub outputends: bool,
44}
45impl Default for RustPileupConfig {
46 fn default() -> Self {
47 Self::new(false, 0, 0, true, true)
48 }
49}
50fn getreader<T>(
51 reader: T,
52 assemblyreader: Option<T>,
53 index: Option<T>,
54 assemblyreaderindex: Option<T>,
55) -> bam::Result<(bam::IndexedReader, Option<faidx::Reader>)>
56where
57 T: AsRef<Path>,
58{
59 let (reader, assemblyreader) = match (reader, assemblyreader, index, assemblyreaderindex) {
60 (a, Some(b), None, None) => (bam::IndexedReader::from_path_and_index(a, b)?, None),
61 (a, None, None, None) => (bam::IndexedReader::from_path(a)?, None),
62 (a, None, Some(c), None) => (
63 bam::IndexedReader::from_path(a)?,
64 Some(faidx::Reader::from_path(c)?),
65 ),
66 (a, None, Some(c), Some(d)) => (
67 bam::IndexedReader::from_path(a)?,
68 Some(faidx::Reader::from_path_and_index(c, d)?),
69 ),
70 (a, Some(b), Some(c), None) => (
71 bam::IndexedReader::from_path_and_index(a, b)?,
72 Some(faidx::Reader::from_path(c)?),
73 ),
74 (a, Some(b), Some(c), Some(d)) => (
75 bam::IndexedReader::from_path_and_index(a, b)?,
76 Some(faidx::Reader::from_path_and_index(c, d)?),
77 ),
78 (.., None, Some(a)) => {
79 return Err(bam::Error::BamInvalidIndex {
80 target: a.as_ref().display().to_string(),
81 });
82 }
83 };
84 Ok((reader, assemblyreader))
85}
86impl RustPileupConfig {
87 pub fn new(
89 onlyprimary: bool,
90 minqual: u8,
91 minscore: u8,
92 reversedel: bool,
93 outputends: bool,
94 ) -> Self {
95 Self {
96 onlyprimary,
97 recalculateBAM: false,
98 minqual,
99 minscore,
100 reversedel,
101 outputends,
102 }
103 }
104 #[allow(non_snake_case, unused)]
106 pub fn getBAMrecalculate(&self) -> bool {
107 self.recalculateBAM
108 }
109 pub fn isonlyprimary(&self) -> bool {
110 self.onlyprimary
111 }
112 #[allow(unused)]
113 pub fn setonlyprimary(&mut self, primary: bool) {
114 self.onlyprimary = primary;
115 }
116 pub fn getminqual(&self) -> u8 {
117 self.minqual
118 }
119 #[allow(unused)]
120 pub fn setminqual(&mut self, minqual: u8) {
121 self.minqual = minqual;
122 }
123 pub fn getminscore(&self) -> u8 {
124 self.minscore
125 }
126 #[allow(unused)]
127 pub fn setminscore(&mut self, minscore: u8) {
128 self.minscore = minscore;
129 }
130 pub fn hasreversedel(&self) -> bool {
131 self.reversedel
132 }
133 #[allow(unused)]
134 pub fn setreversedel(&mut self, reversedel: bool) {
135 self.reversedel = reversedel;
136 }
137 pub fn hasoutputends(&self) -> bool {
138 self.outputends
139 }
140 #[allow(unused)]
141 pub fn setoutputends(&mut self, outputends: bool) {
142 self.outputends = outputends;
143 }
144}
145#[derive(Debug)]
146pub struct RustPileups<'a> {
147 pileup: HashMap<(String, i64), RustPileup>,
148 pub config: RustPileupConfig,
149 region: FetchDefinition<'a>,
150 assemblyreader: Option<faidx::Reader>,
151}
152#[derive(Debug, Eq, PartialEq, Clone)]
153pub struct RustPileup {
154 nbam: u64,
155 index: Option<u64>,
156 pub chrom_name: String,
157 pub pos: u64,
158 pub base: char,
159 pub nreads: Vec<u64>,
160 pub rbases: Vec<String>,
161 pub qualities: Vec<Vec<u8>>,
162}
163impl Ord for RustPileup {
164 fn cmp(&self, other: &Self) -> Ordering {
165 match self.getchrom().cmp(&other.getchrom()) {
166 Ordering::Equal => self.getpos().cmp(&other.pos),
167 ord => ord,
168 }
169 }
170}
171impl PartialOrd for RustPileup {
172 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
173 Some(self.cmp(&other))
174 }
175}
176macro_rules! zip {
177 ($x: expr) => ($x);
178 ($x: expr, $($y: expr), +) => (
179 $x.iter().zip(
180 zip!($($y), +))
181 )
182}
183
184impl RustPileup {
185 pub fn getbam(&self) -> u64 {
187 self.nbam
188 }
189 fn getindex(&self) -> Option<u64> {
191 self.index
192 }
193 fn getindexunchecked(&self) -> u64 {
195 self.index.unwrap()
196 }
197 fn setindex(&mut self, index: u64) -> bam::Result<()> {
198 if index >= self.nbam {
199 return Err(bam::Error::BamPileup);
200 } else {
201 self.index = Some(index);
202 return Ok(());
203 }
204 }
205 pub fn setbam(&mut self, num: u64, alignindex: bool) {
207 self.nbam = num;
208 if alignindex {
209 self.setindex(num.saturating_sub(1))
210 .unwrap_or_else(|_| unreachable!("Not reachable"));
211 }
212 }
213 pub fn getchrom(&self) -> &str {
215 &self.chrom_name
216 }
217 pub fn setchro(&mut self, chro: String) {
219 self.chrom_name = chro;
220 }
221 pub fn getpos(&self) -> u64 {
223 self.pos
224 }
225 pub fn setpos(&mut self, pos: u64) {
227 self.pos = pos;
228 }
229 pub fn getbase(&self) -> char {
231 self.base
232 }
233 pub fn setbase(&mut self, char: char) {
235 self.base = char;
236 }
237 pub fn getnreads(&self) -> &[u64] {
239 &self.nreads
240 }
241 pub fn setnreads(&mut self, index: Option<usize>, reads: u64) -> Result<()> {
243 match index {
244 Some(a) if let Some(b) = self.nreads.get_mut(a) => {
245 *b = reads;
246 Ok(())
247 }
248 Some(b) if b == self.getnreads().len() => {
249 self.nreads.push(reads);
250 Ok(())
251 }
252 None => {
253 self.nreads.push(reads);
254 Ok(())
255 }
256 Some(_) => Err(crate::errors::Error::BamInvalidRecord),
257 }
258 }
259 pub fn getbasereads(&self) -> &[String] {
261 &self.rbases
262 }
263 pub fn setbasereads(&mut self, index: Option<usize>, reads: String) -> Result<()> {
265 match index {
266 Some(a) if let Some(b) = self.rbases.get_mut(a) => {
267 *b = reads;
268 Ok(())
269 }
270 Some(b) if b == self.rbases.len() => {
271 self.rbases.push(reads);
272 Ok(())
273 }
274 Some(_) => Err(crate::errors::Error::BamInvalidRecord),
275 None => {
276 self.rbases.push(reads);
277 Ok(())
278 }
279 }
280 }
281 pub fn getqualities(&self) -> &[Vec<u8>] {
283 &self.qualities
284 }
285 pub fn addqualities(&mut self, index: Option<usize>, qual: u8) -> Result<()> {
287 match index {
288 Some(a) if let Some(b) = self.qualities.get_mut(a) => {
289 b.push(qual);
290 Ok(())
291 }
292 Some(b) if b == self.getqualities().len() => {
293 self.qualities.push(vec![qual]);
294 Ok(())
295 }
296 Some(_) => Err(crate::errors::Error::BamInvalidRecord),
297 None => {
298 self.qualities.push(vec![qual]);
299 Ok(())
300 }
301 }
302 }
303 pub fn setqualities(&mut self, index: Option<usize>, qual: Vec<u8>) -> Result<()> {
305 match index {
306 Some(a) if let Some(b) = self.qualities.get_mut(a) => {
307 *b = qual;
308 Ok(())
309 }
310 Some(b) if b == self.getqualities().len() => {
311 self.qualities.push(qual);
312 Ok(())
313 }
314 Some(_) => Err(crate::errors::Error::BamInvalidRecord),
315 None => {
316 self.qualities.push(qual);
317 Ok(())
318 }
319 }
320 }
321 pub fn resetqualities(&mut self, index: usize) -> Result<()> {
323 if let Some(b) = self.qualities.get_mut(index) {
324 b.clear();
325 b.shrink_to_fit();
326 Ok(())
327 } else {
328 Err(crate::errors::Error::BamInvalidRecord)
329 }
330 }
331}
332impl Default for RustPileup {
333 fn default() -> Self {
334 Self {
335 nbam: 0,
336 index: None,
337 chrom_name: "N/A".to_string(),
338 pos: 0,
339 base: 'N',
340 nreads: Vec::new(),
341 rbases: Vec::new(),
342 qualities: Vec::new(),
343 }
344 }
345}
346impl Display for RustPileup {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 let mut text = String::new();
349 text.push_str(&format!("{}\t", self.getchrom()));
350 text.push_str(&format!("{}\t", self.getpos()));
351 text.push_str(&format!("{}\t", self.getbase()));
352 for (bread, (nreads, qual)) in
353 zip!(self.getbasereads(), self.getnreads(), self.getqualities())
354 {
355 let qual = if !qual.is_empty() {
356 qual.iter().fold(String::new(), |mut acc, qual| {
357 let val = if *qual == u8::MAX { 0 } else { *qual };
358 let val = char::from_u32(u32::from(val).saturating_add(33)).unwrap_or('!');
359 acc.push_str(&format!("{}", val));
360 acc
361 })
362 } else {
363 "*".to_string()
364 };
365 let bread = if bread.trim().len() == 0 {
366 "*".to_string()
367 } else {
368 bread.to_string()
369 };
370 text.push_str(&format!("{}\t{}\t{}\t", nreads, bread, qual));
371 }
372 text = text.trim().to_string();
373 write!(f, "{}", text)
374 }
375}
376impl<'a> Display for RustPileups<'a> {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 let mut text = String::new();
379 for pile in self.into_iter() {
380 text.push_str(&format!("{}\n", pile.to_string()));
381 }
382 write!(f, "{}", text.trim())
383 }
384}
385#[must_use]
386fn casereverse<T>(record: &bam::Record, text: T) -> String
387where
388 T: AsRef<str>,
389{
390 if record.is_reverse() {
391 text.as_ref().to_ascii_lowercase()
392 } else {
393 text.as_ref().to_ascii_uppercase()
394 }
395}
396#[must_use]
397fn casecharreverse(record: &bam::Record, text: char) -> char {
398 if record.is_reverse() {
399 text.to_ascii_lowercase()
400 } else {
401 text.to_ascii_uppercase()
402 }
403}
404#[cfg(feature = "pileuprayon")]
406fn split_range(start: i64, max: i64, n_chunks: usize) -> Vec<(i64, i64)> {
407 let n_chunks = n_chunks.max(1);
408 let total = max.saturating_sub(start).max(0) as usize;
409 if total == 0 {
410 return vec![(start, max)];
411 }
412 let chunk_size = ((total + n_chunks - 1) / n_chunks) as i64; let mut ranges = Vec::new();
414 let mut cur = start;
415 while cur < max {
416 let end = (cur + chunk_size).min(max);
417 ranges.push((cur, end));
418 cur = end;
419 }
420 ranges
421}
422
423#[cfg(feature = "pileuprayon")]
426fn process_chunk<'a, T>(
427 bam_path: T,
428 bam_index: Option<T>,
429 assembly_path: Option<T>,
430 assembly_path_index: Option<T>,
431 config: Option<RustPileupConfig>,
432 actualpileup: Option<&mut RustPileups>,
433 region: FetchDefinition<'a>,
434) -> bam::Result<Option<RustPileups<'a>>>
435where
436 T: AsRef<Path> + Send + Sync + Clone,
437{
438 let (contigname, chunk_start, chunk_max) = match region.clone() {
439 RegionString(a, b, c) => (String::from_utf8_lossy(a), b, c),
440 _ => return Err(bam::Error::Fetch),
441 };
442 let (reader, assemblyreader) =
443 getreader(bam_path, bam_index, assembly_path, assembly_path_index)?;
444 match (config, actualpileup) {
445 (Some(config), None) => {
446 let mut pileup = RustPileups {
447 pileup: HashMap::new(),
448 region,
449 config,
450 assemblyreader: None,
451 };
452 for i in chunk_start..chunk_max {
453 pileup.pileup.insert(
454 (contigname.to_string(), i.saturating_sub(1)),
455 RustPileup::default(),
456 );
457 }
458 pileup.goonrecord(reader, assemblyreader)?;
459 Ok(Some(pileup))
460 }
461 (None, Some(b)) => {
462 b.goonrecord(reader, assemblyreader)?;
463 Ok(None)
464 }
465 _ => return Err(bam::Error::BamInvalidRecord),
466 }
467}
468impl<'a> RustPileups<'a> {
469 pub fn addextrabam<T>(&mut self, reader: T, index: Option<T>) -> bam::Result<()>
471 where
472 T: AsRef<Path>,
473 {
474 let (name, start, max) = match &self.region {
475 RegionString(name, start, end) => (name, *start, *end),
476 _ => return Err(bam::Error::Fetch),
477 };
478 {
480 let mut reader = getreader(reader, index, None, None).map(|(a, _)| a)?;
481 for i in start..=max {
482 if let Some(a) = self.pileup.get_mut(&(
483 String::from_utf8_lossy(name).to_string(),
484 i.saturating_sub(1),
485 )) {
486 a.setbam(a.getbam() + 1, true);
487 a.setnreads(a.getindex().and_then(|f| f.try_into().ok()), 0)?;
488 } else {
489 return Err(Error::BamInvalidRecord);
490 }
491 }
492 let _ = reader.set_threads(
493 available_parallelism()
494 .unwrap_or(std::num::NonZero::new(1).unwrap())
495 .get(),
496 );
497 self.goonrecord(reader, None)?;
498 }
499 Ok(())
533 }
534 fn goonrecord(
535 &mut self,
536 mut reader: bam::IndexedReader,
537 assemblyreader: Option<faidx::Reader>,
538 ) -> bam::Result<()> {
539 let pileup = &mut self.pileup;
540 let config = &self.config;
541 let mut record = bam::Record::new();
542 reader.fetch(self.region.clone())?;
543 match (self.assemblyreader.is_some(), assemblyreader) {
544 (_, Some(a)) => {
545 self.assemblyreader = Some(a);
546 }
547 (true, _) => (),
548 (false, None) => {
549 self.assemblyreader = None;
550 }
551 };
552 let (name, start, max) = match self.region.clone() {
553 RegionString(a, b, c) => (String::from_utf8_lossy(a), b, c),
554 _ => return Err(bam::Error::Fetch),
555 };
556 while let Some(v) = reader.read(&mut record) {
557 if v.is_err() {
558 continue;
559 }
560 if (config.isonlyprimary() && !record.is_primary())
561 || (record.mapq() < config.getminscore())
562 {
563 continue;
564 }
565 let records: Vec<([Option<i64>; 2], Cigar, u8)> =
566 IterAlignedPairsFullCigar::new(record.aligned_pairs_full(), &record).collect();
567 let mut currentgread = 0;
568 for (rangeindex, ([rread, gread], cigar, qual)) in records.iter().enumerate() {
569 let gread = match gread {
570 Some(a) => *a,
571 None if currentgread != 0 => currentgread,
572 _ => continue,
573 };
574 if gread < start.saturating_sub(1) {
575 continue;
576 }
577 if gread > max.saturating_sub(1) {
578 break;
579 }
580 let rread = match rread {
581 Some(a) => Some(*a),
582 None => records
583 .iter()
584 .skip(rangeindex)
585 .find(|p| &p.1 != cigar)
586 .and_then(|([rread, _], _, _)| match rread {
587 Some(a) => Some(*a),
588 None => None,
589 }),
590 };
591 currentgread = gread;
592 let base = match &self.assemblyreader {
593 Some(reader) => reader
594 .fetch_seq_string(
595 &name,
596 gread.try_into().unwrap_or_default(),
597 gread.try_into().unwrap_or_default(),
598 )
599 .map_or('n', |d| d.chars().next().unwrap_or('n')),
600 _ => 'n',
601 };
602 let entry = pileup
603 .entry((name.to_string(), gread))
604 .or_insert(RustPileup::default());
605 if entry.getindex().is_none() && entry.getbam() == 0 {
606 entry.setbam(1, true);
607 entry.setchro(name.to_string());
608 entry.setbase(base);
609 entry.setpos(gread.saturating_add(1).try_into().unwrap_or(u64::MIN));
610 }
611 let hit = entry.getindex().map(|a| a as usize);
613 let index = entry.getindexunchecked() as usize;
614 if entry.getnreads().get(index).is_none_or(|p| p == &0) {
615 entry.setbasereads(Some(index), String::new())?;
616 entry.setqualities(Some(index), vec![])?;
617 if entry.getnreads().get(index).is_none() {
618 entry.setnreads(Some(index), 0)?;
619 }
620 }
621 let initial: u64 = match hit {
622 Some(a) => entry.getnreads().get(a).copied().unwrap_or(0),
623 None => 0,
624 };
625 let mut readbase = if let Some(rread) = rread {
626 record
627 .seq()
628 .rangeextract(rread as usize..rread.saturating_add(1) as usize)
629 .unwrap_or(std::borrow::Cow::Borrowed("n"))
630 .chars()
631 .next()
632 .unwrap_or('n')
633 } else {
634 'n'
635 };
636 readbase = casecharreverse(&record, readbase);
637 if *qual <= config.getminqual() {
638 continue;
639 }
640 if matches!(cigar, Cigar::Ins(_))
641 && let Some(_rread) = rread
642 {
643 () } else {
645 entry.setnreads(hit, initial.saturating_add(1))?;
646 entry.addqualities(hit, *qual)?;
647 }
648 if record.reference_start() == gread && config.hasoutputends() {
649 let mut info = entry
650 .getbasereads()
651 .get(index)
652 .map_or(String::new(), |f| f.to_string());
653 info.push_str(&format!(
654 "^{}",
655 char::from_u32(u32::from(record.mapq().saturating_add(33))).unwrap_or('!')
656 ));
657 entry.setbasereads(hit, info)?;
658 } else if record.reference_end() == gread && config.hasoutputends() {
659 let mut info = entry
660 .getbasereads()
661 .get(index)
662 .map_or(String::new(), |f| f.to_string());
663 info.push_str(&format!(
664 "{}$",
665 char::from_u32(u32::from(record.mapq().saturating_add(33))).unwrap_or('!')
666 ));
667 entry.setbasereads(hit, info)?;
668 }
669 match cigar {
670 Cigar::Equal(_) | Cigar::Match(_) if readbase.eq_ignore_ascii_case(&base) => {
671 let mut info = entry
672 .getbasereads()
673 .get(index)
674 .map_or(String::new(), |f| f.to_string());
675 info.push_str(if record.is_reverse() { "," } else { "." });
676 entry.setbasereads(Some(index), info)?;
677 }
678 Cigar::Diff(_) | Cigar::Match(_) | Cigar::Equal(_) => {
688 let mut info = entry
689 .getbasereads()
690 .get(index)
691 .map_or(String::new(), |f| f.to_string());
692 info.push_str(&readbase.to_string());
693 entry.setbasereads(Some(index), info)?;
694 }
695 Cigar::Ins(n) if let Some(_rread) = rread => {
696 ()
698 }
713 Cigar::Ins(_) => {
714 return Err(bam::Error::BamParseCigar {
715 msg: format!(
716 "Insertion in cigar does not match reference at position {} for read {}",
717 gread,
718 String::from_utf8_lossy(record.name())
719 ),
720 });
721 }
722 Cigar::Del(_) => {
723 let mut info = entry
724 .getbasereads()
725 .get(index)
726 .map_or(String::new(), |f| f.to_string());
727 if record.is_reverse() && config.hasreversedel() {
728 info.push_str("#");
729 } else {
730 info.push_str("*");
731 }
732 entry.setbasereads(Some(index), info)?;
733 }
734 _ => {
735 let mut info = entry
736 .getbasereads()
737 .get(index)
738 .map_or(String::new(), |f| f.to_string());
739 info.push_str("N");
740 entry.setbasereads(Some(index), info)?;
741 }
742 }
743 if let Some(([rstart, gstart], cigar, _)) =
744 records.iter().skip(rangeindex).find(|p| &p.1 != cigar)
745 && (gstart.is_some_and(|f| f.abs_diff(gread) <= 1)
746 || rstart.is_some_and(|f| f.abs_diff(rread.unwrap_or_default()) <= 1))
747 {
749 match cigar {
750 Cigar::Del(n) => {
751 let val = match (&self.assemblyreader, gstart) {
752 (Some(reader), Some(gstart)) => reader
753 .fetch_seq_string(
754 &name,
755 (*gstart).try_into().unwrap_or_default(),
756 gstart
757 .saturating_add(
758 (*n).saturating_sub(1)
759 .try_into()
760 .unwrap_or_default(),
761 )
762 .try_into()
763 .unwrap(),
764 )
765 .unwrap_or(
766 "N".repeat((*n).try_into().unwrap_or_default()).to_string(),
767 ),
768 _ => "N".repeat((*n).try_into().unwrap_or_default()).to_string(),
769 };
770 let val: std::borrow::Cow<str> =
771 std::borrow::Cow::Owned(casereverse(&record, val));
772 let mut info = entry
773 .getbasereads()
774 .get(index)
775 .map_or(String::new(), |f| f.to_string());
776 info.push_str(&format!("-{}{}", n, val));
777 entry.setbasereads(Some(index), info)?;
778 }
779 Cigar::Ins(n) => {
780 let mut info = entry
781 .getbasereads()
782 .get(index)
783 .map_or(String::new(), |f| f.to_string());
784 let recordseq = record.seq();
785 let val = match rstart {
786 Some(rstart) => recordseq
787 .rangeextract(
788 *rstart as usize
789 ..rstart.saturating_add((*n) as i64) as usize,
790 )
791 .unwrap_or(std::borrow::Cow::Owned(
792 "N".repeat((*n).try_into().unwrap_or_default()),
793 )),
794 _ => std::borrow::Cow::Owned(
795 "N".repeat((*n).try_into().unwrap_or_default()),
796 ),
797 };
798 let val: std::borrow::Cow<str> =
799 std::borrow::Cow::Owned(casereverse(&record, val));
800 info.push_str(&format!("+{}{}", n, val));
801 entry.setbasereads(Some(index), info)?;
802 }
803 _ => (),
804 }
805 }
806 }
807 }
808 Ok(())
809 }
810 pub fn new<T>(
831 #[allow(unused_mut)] mut reader: T,
832 #[allow(unused_mut)] mut index: Option<T>,
833 assemblyreader: Option<T>,
834 #[allow(unused_mut)] mut assemblyreaderindex: Option<T>,
835 region: bam::FetchDefinition<'a>,
836 config: RustPileupConfig,
837 ) -> bam::Result<Self>
838 where
839 T: AsRef<Path> + Send + Sync + Clone,
840 {
841 if config.recalculateBAM {
842 unimplemented!("Recalculate BAM is not implemented");
843 }
844 let (name, start, max) = match ®ion {
845 RegionString(name, start, end) => (name, *start, *end),
846 _ => return Err(bam::Error::Fetch),
847 };
848 #[cfg(not(feature = "pileuprayon"))]
849 let pileup = {
850 let (reader, assemblyindex) =
851 getreader(reader, index, assemblyreader, assemblyreaderindex)?;
852 let mut pileup = RustPileups {
853 pileup: HashMap::new(),
854 region: region.clone(),
855 config,
856 assemblyreader: None,
857 };
858 for i in start..=max {
859 pileup.pileup.insert(
860 (
861 String::from_utf8_lossy(name).to_string(),
862 i.saturating_sub(1),
863 ),
864 RustPileup::default(),
865 );
866 }
867 pileup.goonrecord(reader, assemblyindex)?;
868 pileup
869 };
870 #[cfg(feature = "pileuprayon")]
871 let pileup = {
872 let n_chunks = rayon::current_num_threads();
873 let chunk_ranges = split_range(start, max, n_chunks);
874 let chunk_pileups = chunk_ranges
875 .par_iter()
876 .map(|(chunk_start, chunk_max)| {
877 let region = FetchDefinition::RegionString(name, *chunk_start, *chunk_max);
878 let val = process_chunk(
879 reader.clone(),
880 index.clone(),
881 assemblyreader.clone(),
882 assemblyreaderindex.clone(),
883 Some(config),
884 None,
885 region,
886 );
887 val
888 })
889 .collect::<Result<Vec<_>, _>>()?
890 .into_iter()
891 .collect::<Option<Vec<_>>>()
892 .ok_or_else(|| bam::Error::BamInvalidRecord)?;
893 let mut row = chunk_pileups.into_iter();
894 let mut pileup: RustPileups = row.next().unwrap();
895 pileup.region = region;
896 for chunk_map in row {
897 pileup.extend(chunk_map);
898 }
899 pileup
900 };
901 Ok(pileup)
902 }
903}
904impl<'a> Extend<RustPileup> for RustPileups<'a> {
905 fn extend<T: IntoIterator<Item = RustPileup>>(&mut self, iter: T) {
906 for elem in iter {
907 match self.pileup.entry((
908 elem.chrom_name.clone(),
909 elem.pos.saturating_sub(1).try_into().unwrap_or_default(),
910 )) {
911 Vacant(a) => {
912 a.insert(elem);
913 }
914 Occupied(mut b) => {
915 let entry = b.get_mut();
916 if *entry == elem {
917 continue;
918 }
919 let index = entry.getindex().and_then(|a| usize::try_from(a).ok());
920 if let Some(newindex) = index
921 && let (Some(nread), Some(bread), Some(mut qual)) = (
922 elem.getnreads().first(),
923 elem.getbasereads().first(),
924 elem.qualities.clone().get_mut(0),
925 )
926 {
927 let _ = entry.setnreads(
928 index,
929 entry.getnreads().get(newindex).map_or(0, |f| *f) + (*nread),
930 );
931 let _ = entry.setbasereads(
932 index,
933 format!(
934 "{}{}",
935 entry
936 .getbasereads()
937 .get(newindex)
938 .map_or(String::new(), |f: &String| f.to_string()),
939 bread
940 ),
941 );
942 let mut new = Vec::new();
943 let veco = entry.qualities.get_mut(newindex).unwrap_or(&mut new);
944 veco.append(&mut qual);
945 }
946 }
947 }
948 }
949 }
950}
951impl<'a> IntoIterator for RustPileups<'a> {
952 type IntoIter = std::vec::IntoIter<Self::Item>;
953 type Item = RustPileup;
954 fn into_iter(self) -> Self::IntoIter {
955 let mut vec: Vec<RustPileup> = self.pileup.into_values().collect();
956 if !vec.is_sorted() {
957 vec.sort_unstable();
958 }
959 vec.into_iter()
960 }
961}
962impl<'a> IntoIterator for &RustPileups<'a> {
963 type IntoIter = std::vec::IntoIter<Self::Item>;
964 type Item = &'a RustPileup;
965 fn into_iter(self) -> Self::IntoIter {
966 let mut vec: Vec<&'a RustPileup> = self
967 .pileup
968 .values()
969 .map(|v| unsafe { &*(v as *const RustPileup) })
970 .collect();
971 if !vec.is_sorted() {
972 vec.sort_unstable();
973 }
974 vec.into_iter()
975 }
976}
977pub type Alignments<'a> = iter::Map<
979 slice::Iter<'a, htslib::bam_pileup1_t>,
980 fn(&'a htslib::bam_pileup1_t) -> Alignment<'a>,
981>;
982
983#[derive(Debug)]
985pub struct Pileup {
986 inner: *const htslib::bam_pileup1_t,
987 depth: u32,
988 tid: u32,
989 pos: u32,
990}
991
992impl Pileup {
993 pub fn tid(&self) -> u32 {
994 self.tid
995 }
996
997 pub fn pos(&self) -> u32 {
998 self.pos
999 }
1000
1001 pub fn depth(&self) -> u32 {
1002 self.depth
1003 }
1004
1005 pub fn alignments(&self) -> Alignments<'_> {
1006 self.inner().iter().map(Alignment::new)
1007 }
1008
1009 fn inner(&self) -> &[htslib::bam_pileup1_t] {
1010 unsafe {
1011 slice::from_raw_parts(
1012 self.inner as *mut htslib::bam_pileup1_t,
1013 self.depth as usize,
1014 )
1015 }
1016 }
1017}
1018
1019pub struct Alignment<'a> {
1021 inner: &'a htslib::bam_pileup1_t,
1022}
1023
1024impl fmt::Debug for Alignment<'_> {
1025 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1026 write!(f, "Alignment")
1027 }
1028}
1029
1030impl<'a> Alignment<'a> {
1031 pub fn new(inner: &'a htslib::bam_pileup1_t) -> Self {
1032 Alignment { inner }
1033 }
1034
1035 pub fn qpos(&self) -> Option<usize> {
1037 if self.is_del() || self.is_refskip() {
1038 None
1040 } else {
1041 Some(self.inner.qpos as usize)
1042 }
1043 }
1044
1045 pub fn indel(&self) -> Indel {
1047 match self.inner.indel {
1048 len if len < 0 => Indel::Del(-len as u32),
1049 len if len > 0 => Indel::Ins(len as u32),
1050 _ => Indel::None,
1051 }
1052 }
1053
1054 pub fn is_del(&self) -> bool {
1056 self.inner.is_del() != 0
1057 }
1058
1059 pub fn is_head(&self) -> bool {
1061 self.inner.is_head() != 0
1062 }
1063
1064 pub fn is_tail(&self) -> bool {
1066 self.inner.is_tail() != 0
1067 }
1068
1069 pub fn is_refskip(&self) -> bool {
1071 self.inner.is_refskip() != 0
1072 }
1073
1074 pub fn record(&self) -> record::Record {
1076 record::Record::from_inner(self.inner.b)
1077 }
1078}
1079
1080#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)]
1081pub enum Indel {
1082 Ins(u32),
1083 Del(u32),
1084 None,
1085}
1086
1087#[derive(Debug)]
1089pub struct Pileups<'a, R: bam::Read> {
1090 #[allow(dead_code)]
1091 reader: &'a mut R,
1092 itr: htslib::bam_plp_t,
1093}
1094
1095impl<'a, R: bam::Read> Pileups<'a, R> {
1096 pub fn new(reader: &'a mut R, itr: htslib::bam_plp_t) -> Self {
1097 Pileups { reader, itr }
1098 }
1099
1100 pub fn set_max_depth(&mut self, depth: u32) {
1103 if depth > i32::MAX as u32 {
1104 panic!(
1105 "Maximum value for pileup depth is {} but {} was provided",
1106 i32::MAX,
1107 depth
1108 )
1109 }
1110 let intdepth = depth as i32;
1111 unsafe {
1112 htslib::bam_plp_set_maxcnt(self.itr, intdepth);
1113 }
1114 }
1115}
1116
1117impl<R: bam::Read> Iterator for Pileups<'_, R> {
1118 type Item = Result<Pileup>;
1119
1120 #[allow(clippy::match_bool)]
1121 fn next(&mut self) -> Option<Result<Pileup>> {
1122 let (mut tid, mut pos, mut depth) = (0i32, 0i32, 0i32);
1123 let inner = unsafe { htslib::bam_plp_auto(self.itr, &mut tid, &mut pos, &mut depth) };
1124
1125 match inner.is_null() {
1126 true if depth == -1 => Some(Err(Error::BamPileup)),
1127 true => None,
1128 false => Some(Ok(Pileup {
1129 inner,
1130 depth: depth as u32,
1131 tid: tid as u32,
1132 pos: pos as u32,
1133 })),
1134 }
1135 }
1136}
1137
1138impl<R: bam::Read> Drop for Pileups<'_, R> {
1139 fn drop(&mut self) {
1140 unsafe {
1141 htslib::bam_plp_reset(self.itr);
1142 htslib::bam_plp_destroy(self.itr);
1143 }
1144 }
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149
1150 use std::fs::{self};
1151 use std::path::PathBuf;
1152
1153 use crate::bam;
1154 use crate::bam::pileup::{RustPileupConfig, RustPileups};
1155 use crate::bam::{FetchDefinition, Read};
1156 #[test]
1157 fn testpileup() {
1158 let bam = PathBuf::from("test/locus.bam");
1159 let fasta = PathBuf::from("test/locus.fasta");
1160 let pos = FetchDefinition::RegionString("chr14".as_bytes(), 99812480, 99815480);
1162 let config = RustPileupConfig::default();
1163 let po = RustPileups::new(bam, None, Some(fasta), None, pos, config).unwrap();
1164 let val = fs::read_to_string("test/locus.pileup").unwrap();
1165 assert_eq!(val.trim(), po.to_string(), "Pileup does not match");
1166 }
1167 #[test]
1168 fn test_multi_pileup() {
1169 let bam = PathBuf::from("test/locus.bam");
1170 let bam2 = "test/locusmd.bam";
1171 let fasta = PathBuf::from("test/locus.fasta");
1172 let pos = FetchDefinition::RegionString("chr14".as_bytes(), 99812480, 99815480);
1174 let config = RustPileupConfig::default();
1175 let mut po = RustPileups::new(bam, None, Some(fasta), None, pos, config).unwrap();
1176 po.addextrabam(bam2, None).unwrap();
1177 assert_eq!(
1178 fs::read_to_string("test/locusmulti.pileup").unwrap().trim(),
1179 po.to_string(),
1180 "Pileup does not match"
1181 );
1182 }
1183 #[test]
1184 fn test_max_pileup() {
1185 let mut bam = bam::Reader::from_path("test/test.bam").unwrap();
1186 let mut p = bam.pileup();
1187 p.set_max_depth(0u32);
1188 p.set_max_depth(800u32);
1189 }
1190
1191 #[test]
1192 #[should_panic]
1193 fn test_max_pileup_to_high() {
1194 let mut bam = bam::Reader::from_path("test/test.bam").unwrap();
1195 let mut p = bam.pileup();
1196 p.set_max_depth((i32::MAX as u32) + 1);
1197 }
1198}