Skip to main content

extended_htslib/bam/
pileup.rs

1// Copyright 2014 Johannes Köster.
2// Licensed under the MIT license (http://opensource.org/licenses/MIT)
3// This file may not be copied, modified, or distributed
4// except according to those terms.
5
6use 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)]
36/// Config for RustPileUp. Number of arguments might change over time and therefore is not exhaustive, please use functions.
37pub 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    /// Configuration for pileup. Recalculate BAM must be off (else panics)
88    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    /// Get BAM recalculation, is not implemented so cannot be changed and must be false
105    #[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    /// Get number of bam
186    pub fn getbam(&self) -> u64 {
187        self.nbam
188    }
189    /// Get current index
190    fn getindex(&self) -> Option<u64> {
191        self.index
192    }
193    /// Get current index unchecked
194    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    /// Set number of bam
206    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    /// Get chromosome name
214    pub fn getchrom(&self) -> &str {
215        &self.chrom_name
216    }
217    /// Set chromosome name
218    pub fn setchro(&mut self, chro: String) {
219        self.chrom_name = chro;
220    }
221    /// Get position
222    pub fn getpos(&self) -> u64 {
223        self.pos
224    }
225    /// Set position
226    pub fn setpos(&mut self, pos: u64) {
227        self.pos = pos;
228    }
229    /// Get base (N if no assembly provided)
230    pub fn getbase(&self) -> char {
231        self.base
232    }
233    /// Set base
234    pub fn setbase(&mut self, char: char) {
235        self.base = char;
236    }
237    /// Get number of reads
238    pub fn getnreads(&self) -> &[u64] {
239        &self.nreads
240    }
241    /// Set number of reads, return error if index does not exist
242    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    /// Get base reads information
260    pub fn getbasereads(&self) -> &[String] {
261        &self.rbases
262    }
263    /// Set base reads information
264    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    /// Get qualities
282    pub fn getqualities(&self) -> &[Vec<u8>] {
283        &self.qualities
284    }
285    /// Add qualities
286    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    /// Set qualities
304    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    /// Reset qualities
322    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/// Splits [start, max) into up to n_chunks contiguous, non-overlapping sub-ranges.
405#[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; // div_ceil
413    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/// Processes one sub-region: opens its own reader, fetches overlapping reads,
424/// and builds a local pileup map for positions strictly inside [chunk_start, chunk_max).
425#[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    /// Add another BAM to existing Pileup config
470    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        //#[cfg(not(feature = "pileuprayon"))]
479        {
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        /*
500        Cannot share faidx
501        #[cfg(feature = "pileuprayon")]
502        {
503            let pileup = {
504                let n_chunks = rayon::current_num_threads();
505                let chunk_ranges = split_range(start, max, n_chunks);
506                let chunk_pileups = chunk_ranges
507                    .par_iter()
508                    .map(|(chunk_start, chunk_max)| {
509                        let region = FetchDefinition::RegionString(name, *chunk_start, *chunk_max);
510                        let val = process_chunk(
511                            reader.as_ref().clone(),
512                            index.map(|b| b.as_ref()),
513                            None,
514                            None,
515                            None,
516                            Some(self),
517                            region,
518                        );
519                        val
520                    })
521                    .collect::<Result<Vec<_>, _>>()?;
522                let mut row = chunk_pileups.into_iter();
523                let mut pileup: RustPileups = row.next().unwrap();
524                pileup.region = region;
525                for chunk_map in row {
526                    pileup.extend(chunk_map);
527                }
528                pileup
529            };
530        }
531        */
532        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                // hit is now per-entry instead of function-scoped:
612                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                    () //Would be counted afterwards
644                } 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::Equal(_) => {
679                        return Err(bam::Error::BamParseCigar {
680                            msg: format!(
681                                "Equal in cigar does not match reference at position {} for read {}",
682                                gread,
683                                String::from_utf8_lossy(record.name())
684                            ),
685                        });
686                    } */
687                    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                        //Would duplicate
697                        ()
698                        /* let mut info = entry
699                            .getbasereads()
700                            .get(index)
701                            .map_or(String::new(), |f| f.to_string());
702                        let recordseq = record.seq();
703                        let val = recordseq
704                            .rangeextract(rread as usize..rread.saturating_add(*n as i64) as usize)
705                            .unwrap_or(std::borrow::Cow::Owned(
706                                "N".repeat((*n).try_into().unwrap_or_default()),
707                            ));
708                        let val: std::borrow::Cow<str> =
709                            std::borrow::Cow::Owned(casereverse(&record, val));
710                        info.push_str(&format!("+{}{}", n, val));
711                        entry.setbasereads(Some(index), info)?; */
712                    }
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                //Last range before next record
748                {
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    /// Generate a new pileup from BAM and assembly. Recalculate BAM is not supported and would panic.
811    /// **In benchmark this function does 400x worse/slower than samtools and is, at this stage, not recommended.**
812    /// # Examples
813    /// ```
814    /// use extended_htslib::bam::pileup::{Pileup, RustPileup, RustPileups,RustPileupConfig};
815    /// use extended_htslib::bam::{FetchDefinition, Read};
816    /// use extended_htslib::{bam, faidx};
817    /// use std::fs;
818    ///
819    ///
820    /// let mut config = RustPileupConfig::default();
821    /// let pos = FetchDefinition::RegionString("chr14".as_bytes(), 99812480, 99815480);
822    /// config.setminqual(0);
823    /// assert_eq!(config.getminqual(),0);
824    /// let po = RustPileups::new("test/locus.bam", None, Some("test/locus.fasta"), None, pos, config).unwrap();
825    /// # println!("{}",po.to_string());
826    /// assert_eq!(po.into_iter().next().unwrap().to_string(),"chr14	99812480	G	1	^],	E");
827    /// ```
828    /// # Errors
829    /// Bad region.
830    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 &region {
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}
977/// Iterator over alignments of a pileup.
978pub 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/// A pileup over one genomic position.
984#[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
1019/// An aligned read in a pileup.
1020pub 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    /// Position within the read. None if either `is_del` or `is_refskip`.
1036    pub fn qpos(&self) -> Option<usize> {
1037        if self.is_del() || self.is_refskip() {
1038            // there is no alignment position in such a case
1039            None
1040        } else {
1041            Some(self.inner.qpos as usize)
1042        }
1043    }
1044
1045    /// Insertion, deletion (with length) if indel starts at next base or None otherwise.
1046    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    /// Whether there is a deletion in the alignment at this position.
1055    pub fn is_del(&self) -> bool {
1056        self.inner.is_del() != 0
1057    }
1058
1059    /// Whether the alignment starts at this position.
1060    pub fn is_head(&self) -> bool {
1061        self.inner.is_head() != 0
1062    }
1063
1064    /// Whether the alignment ends at this position.
1065    pub fn is_tail(&self) -> bool {
1066        self.inner.is_tail() != 0
1067    }
1068
1069    /// Whether this position is marked as refskip in the CIGAR string.
1070    pub fn is_refskip(&self) -> bool {
1071        self.inner.is_refskip() != 0
1072    }
1073
1074    /// The corresponding record.
1075    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/// Iterator over pileups.
1088#[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    /// Warning: because htslib internally uses signed integer for depth this method
1101    /// will panic if `depth` exceeds `i32::MAX`.
1102    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        // samtools mpileup --reverse-del -a -B -Q 0 -q 0 -r "chr14:99812480-99815480" -f test/locus.fasta test/locus.bam > test/locus.pileup
1161        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        // samtools mpileup --reverse-del -a -B -Q 0 -q 0 -r "chr14:99812480-99815480" -f test/locus.fasta test/locus.bam test/locusmd.bam > test/locusmulti.pileup
1173        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}