Skip to main content

fastx/
paired.rs

1//! Paired-end reads, from two files or one interleaved stream.
2//!
3//! Paired data is where sequencing pipelines quietly go wrong: R1 and R2 must
4//! stay in lockstep, and a tool that filters one file without the other, or
5//! reads two files that were sorted differently, produces output that looks
6//! perfectly well-formed and is silently mispaired. [`PairedReader`] checks that
7//! the mate names line up on every pair, so that goes from a subtle wrong answer
8//! to an error on the first record.
9//!
10//! ```
11//! use fastx::paired::PairedReader;
12//!
13//! let r1 = b"@read1/1\nACGT\n+\nIIII\n@read2/1\nTTTT\n+\nIIII\n";
14//! let r2 = b"@read1/2\nCCCC\n+\nIIII\n@read2/2\nGGGG\n+\nIIII\n";
15//!
16//! let mut reader = PairedReader::from_readers(&r1[..], &r2[..]);
17//! let pair = reader.read_pair()?.unwrap();
18//! assert_eq!(pair.first.id, "read1/1");
19//! assert_eq!(pair.second.id, "read1/2");
20//! assert_eq!(reader.count_pairs()?, 1); // one pair left
21//! # Ok::<(), fastx::Error>(())
22//! ```
23
24use std::io::{Read, Write};
25use std::path::Path;
26
27use crate::error::{Error, Result};
28use crate::format::Format;
29use crate::reader::{self, FastxReader};
30use crate::record::Sequence;
31use crate::writer::FastxWriter;
32
33/// Two mates of the same fragment.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct Pair {
36    /// The forward read, from R1.
37    pub first: Sequence,
38    /// The reverse read, from R2.
39    pub second: Sequence,
40}
41
42impl Pair {
43    /// A pair from two records.
44    pub fn new(first: Sequence, second: Sequence) -> Pair {
45        Pair { first, second }
46    }
47
48    /// Combined length of both mates.
49    pub fn len(&self) -> usize {
50        self.first.len() + self.second.len()
51    }
52
53    /// True when neither mate has any residues.
54    pub fn is_empty(&self) -> bool {
55        self.first.is_empty() && self.second.is_empty()
56    }
57
58    /// Reset both records, keeping their allocations.
59    pub fn clear(&mut self) {
60        self.first.clear();
61        self.second.clear();
62    }
63
64    /// Whether the two ids name the same fragment.
65    pub fn names_match(&self) -> bool {
66        mate_stem(&self.first.id) == mate_stem(&self.second.id)
67    }
68}
69
70/// The part of a read name shared by both mates.
71///
72/// Illumina writes the mate number either as a `/1` suffix on the name or as a
73/// separate field after a space — and this crate's ids stop at the first
74/// whitespace, so the second style already gives identical ids. Only the suffix
75/// style needs stripping, in the few spellings that occur in the wild.
76///
77/// ```
78/// # use fastx::paired::mate_stem;
79/// assert_eq!(mate_stem("read1/1"), "read1");
80/// assert_eq!(mate_stem("read1.2"), "read1");
81/// assert_eq!(mate_stem("read1_1"), "read1");
82/// assert_eq!(mate_stem("read1"), "read1");
83/// // A name that merely ends in a digit is left alone.
84/// assert_eq!(mate_stem("read12"), "read12");
85/// ```
86pub fn mate_stem(id: &str) -> &str {
87    let bytes = id.as_bytes();
88    if bytes.len() >= 2 {
89        let last = bytes[bytes.len() - 1];
90        let separator = bytes[bytes.len() - 2];
91        if (last == b'1' || last == b'2') && matches!(separator, b'/' | b'.' | b'_') {
92            return &id[..id.len() - 2];
93        }
94    }
95    id
96}
97
98/// Where a [`PairedReader`] takes its records from.
99// One variant holds two readers and the other holds one, so a size difference is
100// inherent. Boxing would move a couple of hundred bytes to the heap and add an
101// indirection to every record read, to save nothing: exactly one of these exists
102// per `PairedReader`, never a collection of them.
103#[allow(clippy::large_enum_variant)]
104enum Source<R: Read> {
105    /// Separate R1 and R2 files.
106    Split {
107        first: FastxReader<R>,
108        second: FastxReader<R>,
109    },
110    /// One stream with the mates alternating.
111    Interleaved(FastxReader<R>),
112}
113
114/// A reader that yields both mates at once.
115///
116/// By default it verifies that the mate names agree; call
117/// [`PairedReader::check_names`] to turn that off for data with naming that does
118/// not follow any convention.
119pub struct PairedReader<R: Read> {
120    source: Source<R>,
121    check_names: bool,
122    pairs_read: u64,
123}
124
125/// The type [`PairedReader::open`] and [`PairedReader::open_interleaved`] return.
126pub type BoxedPairedReader = PairedReader<Box<dyn Read + Send>>;
127
128impl PairedReader<Box<dyn Read + Send>> {
129    /// Open an R1/R2 pair of files, each transparently decompressed.
130    pub fn open<P: AsRef<Path>, Q: AsRef<Path>>(first: P, second: Q) -> Result<BoxedPairedReader> {
131        Ok(PairedReader {
132            source: Source::Split {
133                first: reader::open(first)?,
134                second: reader::open(second)?,
135            },
136            check_names: true,
137            pairs_read: 0,
138        })
139    }
140
141    /// Open one interleaved file, where R1 and R2 alternate.
142    pub fn open_interleaved<P: AsRef<Path>>(path: P) -> Result<BoxedPairedReader> {
143        Ok(PairedReader {
144            source: Source::Interleaved(reader::open(path)?),
145            check_names: true,
146            pairs_read: 0,
147        })
148    }
149}
150
151impl<R: Read> PairedReader<R> {
152    /// Read a pair from two separate streams.
153    pub fn from_readers(first: R, second: R) -> PairedReader<R> {
154        PairedReader::from_split(FastxReader::new(first), FastxReader::new(second))
155    }
156
157    /// Read pairs from one interleaved stream.
158    pub fn interleaved(inner: R) -> PairedReader<R> {
159        PairedReader::from_interleaved(FastxReader::new(inner))
160    }
161
162    /// Read pairs from a reader that is already configured — from
163    /// [`crate::from_stdin`], say, or with a chosen buffer size.
164    pub fn from_interleaved(reader: FastxReader<R>) -> PairedReader<R> {
165        PairedReader {
166            source: Source::Interleaved(reader),
167            check_names: true,
168            pairs_read: 0,
169        }
170    }
171
172    /// Read pairs from two readers that are already configured.
173    pub fn from_split(first: FastxReader<R>, second: FastxReader<R>) -> PairedReader<R> {
174        PairedReader {
175            source: Source::Split { first, second },
176            check_names: true,
177            pairs_read: 0,
178        }
179    }
180
181    /// Whether to verify that mate names agree. On by default.
182    pub fn check_names(mut self, check: bool) -> Self {
183        self.check_names = check;
184        self
185    }
186
187    /// Number of pairs read so far.
188    pub fn pairs_read(&self) -> u64 {
189        self.pairs_read
190    }
191
192    /// The format being read, once known.
193    pub fn format(&self) -> Option<Format> {
194        match &self.source {
195            Source::Split { first, .. } => first.format(),
196            Source::Interleaved(reader) => reader.format(),
197        }
198    }
199
200    /// Read the next pair into `pair`, reusing its allocations.
201    ///
202    /// Returns `Ok(false)` when both inputs are exhausted together, and an error
203    /// when only one of them is — a truncated mate file is a real problem, not
204    /// an early stop.
205    pub fn read_into(&mut self, pair: &mut Pair) -> Result<bool> {
206        let present = match &mut self.source {
207            Source::Split { first, second } => {
208                let got_first = first.read_into(&mut pair.first)?;
209                let got_second = second.read_into(&mut pair.second)?;
210                match (got_first, got_second) {
211                    (false, false) => return Ok(false),
212                    (true, true) => true,
213                    (true, false) => {
214                        return Err(Error::PairTruncated {
215                            pairs_read: self.pairs_read,
216                            missing: "R2",
217                        })
218                    }
219                    (false, true) => {
220                        return Err(Error::PairTruncated {
221                            pairs_read: self.pairs_read,
222                            missing: "R1",
223                        })
224                    }
225                }
226            }
227            Source::Interleaved(reader) => {
228                if !reader.read_into(&mut pair.first)? {
229                    return Ok(false);
230                }
231                if !reader.read_into(&mut pair.second)? {
232                    return Err(Error::PairTruncated {
233                        pairs_read: self.pairs_read,
234                        missing: "the second mate",
235                    });
236                }
237                true
238            }
239        };
240
241        if self.check_names && !pair.names_match() {
242            return Err(Error::PairMismatch {
243                pairs_read: self.pairs_read,
244                first: pair.first.id.clone(),
245                second: pair.second.id.clone(),
246            });
247        }
248        self.pairs_read += 1;
249        Ok(present)
250    }
251
252    /// Read the next pair into a fresh [`Pair`].
253    pub fn read_pair(&mut self) -> Result<Option<Pair>> {
254        let mut pair = Pair::default();
255        if self.read_into(&mut pair)? {
256            Ok(Some(pair))
257        } else {
258            Ok(None)
259        }
260    }
261
262    /// Run `f` on every remaining pair, reusing one buffer.
263    pub fn for_each_pair<F>(&mut self, mut f: F) -> Result<()>
264    where
265        F: FnMut(&Pair) -> Result<()>,
266    {
267        let mut pair = Pair::default();
268        while self.read_into(&mut pair)? {
269            f(&pair)?;
270        }
271        Ok(())
272    }
273
274    /// Count the remaining pairs.
275    pub fn count_pairs(&mut self) -> Result<u64> {
276        let mut pairs = 0;
277        let mut pair = Pair::default();
278        while self.read_into(&mut pair)? {
279            pairs += 1;
280        }
281        Ok(pairs)
282    }
283}
284
285impl<R: Read> Iterator for PairedReader<R> {
286    type Item = Result<Pair>;
287
288    fn next(&mut self) -> Option<Self::Item> {
289        match self.read_pair() {
290            Ok(Some(pair)) => Some(Ok(pair)),
291            Ok(None) => None,
292            Err(e) => Some(Err(e)),
293        }
294    }
295}
296
297/// A writer for paired reads, to two files or one interleaved stream.
298pub enum PairedWriter<W: Write> {
299    /// Separate R1 and R2 outputs.
300    Split {
301        /// Where forward reads go.
302        first: FastxWriter<W>,
303        /// Where reverse reads go.
304        second: FastxWriter<W>,
305    },
306    /// One output with the mates alternating.
307    Interleaved(FastxWriter<W>),
308}
309
310impl<W: Write> PairedWriter<W> {
311    /// Write one pair.
312    pub fn write_pair(&mut self, pair: &Pair) -> Result<()> {
313        match self {
314            PairedWriter::Split { first, second } => {
315                first.write_record(&pair.first)?;
316                second.write_record(&pair.second)?;
317            }
318            PairedWriter::Interleaved(writer) => {
319                writer.write_record(&pair.first)?;
320                writer.write_record(&pair.second)?;
321            }
322        }
323        Ok(())
324    }
325
326    /// Flush every underlying writer.
327    pub fn flush(&mut self) -> Result<()> {
328        match self {
329            PairedWriter::Split { first, second } => {
330                first.flush()?;
331                second.flush()?;
332            }
333            PairedWriter::Interleaved(writer) => writer.flush()?,
334        }
335        Ok(())
336    }
337
338    /// Flush and finish, surfacing any error from closing a compressed stream.
339    pub fn finish(self) -> Result<()> {
340        match self {
341            PairedWriter::Split { first, second } => {
342                first.finish()?;
343                second.finish()?;
344            }
345            PairedWriter::Interleaved(writer) => {
346                writer.finish()?;
347            }
348        }
349        Ok(())
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    const R1: &[u8] = b"@read1/1\nACGT\n+\nIIII\n@read2/1\nTTTT\n+\nJJJJ\n";
358    const R2: &[u8] = b"@read1/2\nCCCC\n+\nIIII\n@read2/2\nGGGG\n+\nJJJJ\n";
359
360    #[test]
361    fn reads_pairs_from_two_streams() {
362        let mut reader = PairedReader::from_readers(R1, R2);
363        let pairs: Vec<Pair> = reader.by_ref().collect::<Result<Vec<_>>>().unwrap();
364        assert_eq!(pairs.len(), 2);
365        assert_eq!(pairs[0].first.seq, b"ACGT");
366        assert_eq!(pairs[0].second.seq, b"CCCC");
367        assert_eq!(pairs[1].first.id, "read2/1");
368        assert_eq!(reader.pairs_read(), 2);
369    }
370
371    #[test]
372    fn reads_interleaved() {
373        let interleaved = b"@r/1\nAC\n+\nII\n@r/2\nGT\n+\nII\n@s/1\nAA\n+\nII\n@s/2\nTT\n+\nII\n";
374        let pairs: Vec<Pair> = PairedReader::interleaved(&interleaved[..])
375            .collect::<Result<Vec<_>>>()
376            .unwrap();
377        assert_eq!(pairs.len(), 2);
378        assert_eq!(pairs[0].second.seq, b"GT");
379        assert_eq!(pairs[1].first.id, "s/1");
380    }
381
382    #[test]
383    fn catches_mispaired_names() {
384        // R2 sorted differently: the classic silent pipeline bug.
385        let shuffled = b"@read2/2\nGGGG\n+\nJJJJ\n@read1/2\nCCCC\n+\nIIII\n";
386        let error = PairedReader::from_readers(R1, &shuffled[..])
387            .read_pair()
388            .unwrap_err();
389        match error {
390            Error::PairMismatch { first, second, .. } => {
391                assert_eq!((first.as_str(), second.as_str()), ("read1/1", "read2/2"));
392            }
393            other => panic!("expected PairMismatch, got {other}"),
394        }
395
396        // The check can be waived for data with unconventional names.
397        let pairs = PairedReader::from_readers(R1, &shuffled[..])
398            .check_names(false)
399            .count_pairs()
400            .unwrap();
401        assert_eq!(pairs, 2);
402    }
403
404    #[test]
405    fn catches_a_truncated_mate_file() {
406        let short = b"@read1/2\nCCCC\n+\nIIII\n";
407        let error = PairedReader::from_readers(R1, &short[..])
408            .count_pairs()
409            .unwrap_err();
410        assert!(
411            matches!(
412                error,
413                Error::PairTruncated {
414                    missing: "R2",
415                    pairs_read: 1
416                }
417            ),
418            "{error}"
419        );
420
421        let error = PairedReader::from_readers(&short[..], R2)
422            .count_pairs()
423            .unwrap_err();
424        assert!(
425            matches!(error, Error::PairTruncated { missing: "R1", .. }),
426            "{error}"
427        );
428
429        // An odd number of interleaved records is the same problem.
430        let odd = b"@r/1\nAC\n+\nII\n@r/2\nGT\n+\nII\n@s/1\nAA\n+\nII\n";
431        let error = PairedReader::interleaved(&odd[..])
432            .count_pairs()
433            .unwrap_err();
434        assert!(
435            matches!(error, Error::PairTruncated { pairs_read: 1, .. }),
436            "{error}"
437        );
438    }
439
440    #[test]
441    fn accepts_illumina_style_names() {
442        // Modern Illumina puts the mate number after a space, so the ids match
443        // outright once the header is split.
444        let r1 = b"@A00123:1:HXX:1:1101:1000:1000 1:N:0:ATCG\nAC\n+\nII\n";
445        let r2 = b"@A00123:1:HXX:1:1101:1000:1000 2:N:0:ATCG\nGT\n+\nII\n";
446        let pair = PairedReader::from_readers(&r1[..], &r2[..])
447            .read_pair()
448            .unwrap()
449            .unwrap();
450        assert!(pair.names_match());
451        assert_eq!(pair.first.description.as_deref(), Some("1:N:0:ATCG"));
452    }
453
454    #[test]
455    fn mate_stems() {
456        for (id, stem) in [
457            ("read/1", "read"),
458            ("read/2", "read"),
459            ("read.1", "read"),
460            ("read_2", "read"),
461            ("read", "read"),
462            ("read1", "read1"),
463            ("r", "r"),
464            ("", ""),
465            ("/1", ""),
466            ("read/3", "read/3"),
467        ] {
468            assert_eq!(mate_stem(id), stem, "{id}");
469        }
470    }
471
472    #[test]
473    fn empty_input_yields_no_pairs() {
474        assert_eq!(
475            PairedReader::from_readers(&b""[..], &b""[..])
476                .count_pairs()
477                .unwrap(),
478            0
479        );
480        assert_eq!(
481            PairedReader::interleaved(&b""[..]).count_pairs().unwrap(),
482            0
483        );
484    }
485
486    #[test]
487    fn round_trips_through_the_writer() {
488        let mut interleaved = Vec::new();
489        {
490            let mut writer =
491                PairedWriter::Interleaved(FastxWriter::new(&mut interleaved, Format::Fastq));
492            let mut reader = PairedReader::from_readers(R1, R2);
493            reader
494                .for_each_pair(|pair| writer.write_pair(pair))
495                .unwrap();
496            writer.finish().unwrap();
497        }
498
499        let pairs: Vec<Pair> = PairedReader::interleaved(&interleaved[..])
500            .collect::<Result<Vec<_>>>()
501            .unwrap();
502        let original: Vec<Pair> = PairedReader::from_readers(R1, R2)
503            .collect::<Result<Vec<_>>>()
504            .unwrap();
505        assert_eq!(pairs, original);
506    }
507
508    #[test]
509    fn split_writer_separates_the_mates() {
510        let mut first = Vec::new();
511        let mut second = Vec::new();
512        {
513            let mut writer = PairedWriter::Split {
514                first: FastxWriter::new(&mut first, Format::Fastq),
515                second: FastxWriter::new(&mut second, Format::Fastq),
516            };
517            PairedReader::from_readers(R1, R2)
518                .for_each_pair(|pair| writer.write_pair(pair))
519                .unwrap();
520            writer.finish().unwrap();
521        }
522        assert_eq!(first, R1);
523        assert_eq!(second, R2);
524    }
525}