Skip to main content

gwseq_io/
align.rs

1//! One reader over both alignment formats.
2//!
3//! BAM and CRAM answer the same questions with the same types — the same
4//! [`EntriesRequest`], the same [`BamRecord`], the same four read paths — because
5//! the CRAM reader rebuilds BAM records rather than inventing a second entry
6//! type. What is left over is the *dispatch*, and this is it.
7//!
8//! # Why an enum rather than a trait
9//!
10//! The instinct is a trait, and it does not survive contact with the walks.
11//! [`bam::LocusWalk`] and [`cram::LocusWalk`] carry different plans — the BAM
12//! one a set of decompressed-chunk cursors, the CRAM one nothing of the sort —
13//! and the borrowing iterators over them carry lifetimes. An object-safe trait
14//! would mean boxing the plan and giving up the plain `Iterator` a Rust caller
15//! gets today, to abstract over a set that has two members and will not grow a
16//! third. A `match` costs one predictable branch per *window*, not per record.
17//!
18//! This is what the Python layer holds, which is why `gwseq_io.BamReader` reads
19//! CRAM too and reports which it has through its `type` — exactly as
20//! `BbiReader` already serves bigWig and bigBed.
21
22use crate::bam::{self, BamReader, BamRecord, EntriesRequest, SamHeader};
23use crate::cram::{self, CramReader};
24use crate::error::{Error, Result};
25use crate::genomic::ChrMap;
26
27/// A BAM or a CRAM, opened for reading.
28#[allow(clippy::large_enum_variant)]
29#[derive(Debug)]
30pub enum Alignments {
31    Bam(BamReader),
32    Cram(CramReader),
33}
34
35impl Alignments {
36    /// `"bam"` or `"cram"`.
37    pub fn kind(&self) -> &'static str {
38        match self {
39            Self::Bam(_) => "bam",
40            Self::Cram(_) => "cram",
41        }
42    }
43
44    pub fn header(&self) -> &SamHeader {
45        match self {
46            Self::Bam(reader) => reader.header(),
47            Self::Cram(reader) => reader.header(),
48        }
49    }
50
51    pub fn chr_sizes(&self) -> &ChrMap {
52        match self {
53            Self::Bam(reader) => reader.chr_sizes(),
54            Self::Cram(reader) => reader.chr_sizes(),
55        }
56    }
57
58    pub fn is_indexed(&self) -> bool {
59        match self {
60            Self::Bam(reader) => reader.is_indexed(),
61            Self::Cram(reader) => reader.is_indexed(),
62        }
63    }
64
65    pub fn index_error(&self) -> &str {
66        match self {
67            Self::Bam(reader) => reader.index_error(),
68            Self::Cram(reader) => reader.index_error(),
69        }
70    }
71
72    pub fn is_closed(&self) -> bool {
73        match self {
74            Self::Bam(reader) => reader.is_closed(),
75            Self::Cram(reader) => reader.is_closed(),
76        }
77    }
78
79    pub fn path(&self) -> &str {
80        match self {
81            Self::Bam(reader) => reader.path(),
82            Self::Cram(reader) => reader.path(),
83        }
84    }
85
86    pub fn parallel(&self) -> usize {
87        match self {
88            Self::Bam(reader) => reader.parallel(),
89            Self::Cram(reader) => reader.parallel(),
90        }
91    }
92
93    pub fn close(&mut self) {
94        match self {
95            Self::Bam(reader) => reader.close(),
96            Self::Cram(reader) => reader.close(),
97        }
98    }
99
100    /// The CRAM version, or `None` for a BAM.
101    pub fn version(&self) -> Option<(u8, u8)> {
102        match self {
103            Self::Bam(_) => None,
104            Self::Cram(reader) => Some(reader.version()),
105        }
106    }
107
108    /// The reference FASTA in use. Always `None` for a BAM, which needs none.
109    pub fn reference_path(&self) -> Option<&str> {
110        match self {
111            Self::Bam(_) => None,
112            Self::Cram(reader) => reader.reference_path(),
113        }
114    }
115
116    /// Why there is no reference, when there is none and one was wanted.
117    pub fn reference_error(&self) -> &str {
118        match self {
119            Self::Bam(_) => "",
120            Self::Cram(reader) => reader.reference_error(),
121        }
122    }
123
124    pub fn read_entries(&self, req: &EntriesRequest) -> Result<Vec<Vec<BamRecord>>> {
125        match self {
126            Self::Bam(reader) => reader.read_entries(req),
127            Self::Cram(reader) => reader.read_entries(req),
128        }
129    }
130
131    pub fn read_all_entries(&self, req: &EntriesRequest) -> Result<Vec<BamRecord>> {
132        match self {
133            Self::Bam(reader) => reader.read_all_entries(req),
134            Self::Cram(reader) => reader.read_all_entries(req),
135        }
136    }
137}
138
139/// A walk over either format's loci or windows.
140#[derive(Debug)]
141pub enum AlignmentWalk {
142    Bam(bam::LocusWalk),
143    Cram(cram::LocusWalk),
144}
145
146impl AlignmentWalk {
147    /// A per-locus walk.
148    pub fn plan(reader: &Alignments, req: &EntriesRequest) -> Result<Self> {
149        Ok(match reader {
150            Alignments::Bam(reader) => Self::Bam(bam::LocusWalk::plan(reader, req)?),
151            Alignments::Cram(reader) => Self::Cram(cram::LocusWalk::plan(reader, req)?),
152        })
153    }
154
155    /// A walk over windows tiling whole references.
156    pub fn plan_windows(reader: &Alignments, req: &EntriesRequest, span: i64) -> Result<Self> {
157        Ok(match reader {
158            Alignments::Bam(reader) => Self::Bam(bam::LocusWalk::plan_windows(reader, req, span)?),
159            Alignments::Cram(reader) => {
160                Self::Cram(cram::LocusWalk::plan_windows(reader, req, span)?)
161            }
162        })
163    }
164
165    /// The same walk, back at its first locus, sharing the plan.
166    pub fn restarted(&self) -> Self {
167        match self {
168            Self::Bam(walk) => Self::Bam(walk.restarted()),
169            Self::Cram(walk) => Self::Cram(walk.restarted()),
170        }
171    }
172
173    pub fn len(&self) -> usize {
174        match self {
175            Self::Bam(walk) => walk.len(),
176            Self::Cram(walk) => walk.len(),
177        }
178    }
179
180    pub fn is_empty(&self) -> bool {
181        self.len() == 0
182    }
183
184    pub fn order(&self) -> &[usize] {
185        match self {
186            Self::Bam(walk) => walk.order(),
187            Self::Cram(walk) => walk.order(),
188        }
189    }
190
191    /// The next window, from the reader this walk was planned against.
192    ///
193    /// Handing it a reader of the other format is a caller's mistake rather
194    /// than a corrupt file, so it is [`Error::InvalidArgument`] — and it cannot
195    /// happen through the Python layer, which holds the pair together.
196    pub fn next_window(&mut self, reader: &Alignments) -> Option<Result<Vec<BamRecord>>> {
197        match (self, reader) {
198            (Self::Bam(walk), Alignments::Bam(reader)) => walk.next_window(reader),
199            (Self::Cram(walk), Alignments::Cram(reader)) => walk.next_window(reader),
200            _ => Some(Err(Error::invalid(
201                "this walk was planned against a reader of the other format",
202            ))),
203        }
204    }
205}