fastx-io 0.3.0

Fast, streaming FASTA/FASTQ reader and writer for bioinformatics pipelines
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
//! Paired-end reads, from two files or one interleaved stream.
//!
//! Paired data is where sequencing pipelines quietly go wrong: R1 and R2 must
//! stay in lockstep, and a tool that filters one file without the other, or
//! reads two files that were sorted differently, produces output that looks
//! perfectly well-formed and is silently mispaired. [`PairedReader`] checks that
//! the mate names line up on every pair, so that goes from a subtle wrong answer
//! to an error on the first record.
//!
//! ```
//! use fastx::paired::PairedReader;
//!
//! let r1 = b"@read1/1\nACGT\n+\nIIII\n@read2/1\nTTTT\n+\nIIII\n";
//! let r2 = b"@read1/2\nCCCC\n+\nIIII\n@read2/2\nGGGG\n+\nIIII\n";
//!
//! let mut reader = PairedReader::from_readers(&r1[..], &r2[..]);
//! let pair = reader.read_pair()?.unwrap();
//! assert_eq!(pair.first.id, "read1/1");
//! assert_eq!(pair.second.id, "read1/2");
//! assert_eq!(reader.count_pairs()?, 1); // one pair left
//! # Ok::<(), fastx::Error>(())
//! ```

use std::io::{Read, Write};
use std::path::Path;

use crate::error::{Error, Result};
use crate::format::Format;
use crate::reader::{self, FastxReader};
use crate::record::Sequence;
use crate::writer::FastxWriter;

/// Two mates of the same fragment.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Pair {
    /// The forward read, from R1.
    pub first: Sequence,
    /// The reverse read, from R2.
    pub second: Sequence,
}

impl Pair {
    /// A pair from two records.
    pub fn new(first: Sequence, second: Sequence) -> Pair {
        Pair { first, second }
    }

    /// Combined length of both mates.
    pub fn len(&self) -> usize {
        self.first.len() + self.second.len()
    }

    /// True when neither mate has any residues.
    pub fn is_empty(&self) -> bool {
        self.first.is_empty() && self.second.is_empty()
    }

    /// Reset both records, keeping their allocations.
    pub fn clear(&mut self) {
        self.first.clear();
        self.second.clear();
    }

    /// Whether the two ids name the same fragment.
    pub fn names_match(&self) -> bool {
        mate_stem(&self.first.id) == mate_stem(&self.second.id)
    }
}

/// The part of a read name shared by both mates.
///
/// Illumina writes the mate number either as a `/1` suffix on the name or as a
/// separate field after a space — and this crate's ids stop at the first
/// whitespace, so the second style already gives identical ids. Only the suffix
/// style needs stripping, in the few spellings that occur in the wild.
///
/// ```
/// # use fastx::paired::mate_stem;
/// assert_eq!(mate_stem("read1/1"), "read1");
/// assert_eq!(mate_stem("read1.2"), "read1");
/// assert_eq!(mate_stem("read1_1"), "read1");
/// assert_eq!(mate_stem("read1"), "read1");
/// // A name that merely ends in a digit is left alone.
/// assert_eq!(mate_stem("read12"), "read12");
/// ```
pub fn mate_stem(id: &str) -> &str {
    let bytes = id.as_bytes();
    if bytes.len() >= 2 {
        let last = bytes[bytes.len() - 1];
        let separator = bytes[bytes.len() - 2];
        if (last == b'1' || last == b'2') && matches!(separator, b'/' | b'.' | b'_') {
            return &id[..id.len() - 2];
        }
    }
    id
}

/// Where a [`PairedReader`] takes its records from.
// One variant holds two readers and the other holds one, so a size difference is
// inherent. Boxing would move a couple of hundred bytes to the heap and add an
// indirection to every record read, to save nothing: exactly one of these exists
// per `PairedReader`, never a collection of them.
#[allow(clippy::large_enum_variant)]
enum Source<R: Read> {
    /// Separate R1 and R2 files.
    Split {
        first: FastxReader<R>,
        second: FastxReader<R>,
    },
    /// One stream with the mates alternating.
    Interleaved(FastxReader<R>),
}

/// A reader that yields both mates at once.
///
/// By default it verifies that the mate names agree; call
/// [`PairedReader::check_names`] to turn that off for data with naming that does
/// not follow any convention.
pub struct PairedReader<R: Read> {
    source: Source<R>,
    check_names: bool,
    pairs_read: u64,
}

/// The type [`PairedReader::open`] and [`PairedReader::open_interleaved`] return.
pub type BoxedPairedReader = PairedReader<Box<dyn Read + Send>>;

impl PairedReader<Box<dyn Read + Send>> {
    /// Open an R1/R2 pair of files, each transparently decompressed.
    pub fn open<P: AsRef<Path>, Q: AsRef<Path>>(first: P, second: Q) -> Result<BoxedPairedReader> {
        Ok(PairedReader {
            source: Source::Split {
                first: reader::open(first)?,
                second: reader::open(second)?,
            },
            check_names: true,
            pairs_read: 0,
        })
    }

    /// Open one interleaved file, where R1 and R2 alternate.
    pub fn open_interleaved<P: AsRef<Path>>(path: P) -> Result<BoxedPairedReader> {
        Ok(PairedReader {
            source: Source::Interleaved(reader::open(path)?),
            check_names: true,
            pairs_read: 0,
        })
    }
}

impl<R: Read> PairedReader<R> {
    /// Read a pair from two separate streams.
    pub fn from_readers(first: R, second: R) -> PairedReader<R> {
        PairedReader::from_split(FastxReader::new(first), FastxReader::new(second))
    }

    /// Read pairs from one interleaved stream.
    pub fn interleaved(inner: R) -> PairedReader<R> {
        PairedReader::from_interleaved(FastxReader::new(inner))
    }

    /// Read pairs from a reader that is already configured — from
    /// [`crate::from_stdin`], say, or with a chosen buffer size.
    pub fn from_interleaved(reader: FastxReader<R>) -> PairedReader<R> {
        PairedReader {
            source: Source::Interleaved(reader),
            check_names: true,
            pairs_read: 0,
        }
    }

    /// Read pairs from two readers that are already configured.
    pub fn from_split(first: FastxReader<R>, second: FastxReader<R>) -> PairedReader<R> {
        PairedReader {
            source: Source::Split { first, second },
            check_names: true,
            pairs_read: 0,
        }
    }

    /// Whether to verify that mate names agree. On by default.
    pub fn check_names(mut self, check: bool) -> Self {
        self.check_names = check;
        self
    }

    /// Number of pairs read so far.
    pub fn pairs_read(&self) -> u64 {
        self.pairs_read
    }

    /// The format being read, once known.
    pub fn format(&self) -> Option<Format> {
        match &self.source {
            Source::Split { first, .. } => first.format(),
            Source::Interleaved(reader) => reader.format(),
        }
    }

    /// Read the next pair into `pair`, reusing its allocations.
    ///
    /// Returns `Ok(false)` when both inputs are exhausted together, and an error
    /// when only one of them is — a truncated mate file is a real problem, not
    /// an early stop.
    pub fn read_into(&mut self, pair: &mut Pair) -> Result<bool> {
        let present = match &mut self.source {
            Source::Split { first, second } => {
                let got_first = first.read_into(&mut pair.first)?;
                let got_second = second.read_into(&mut pair.second)?;
                match (got_first, got_second) {
                    (false, false) => return Ok(false),
                    (true, true) => true,
                    (true, false) => {
                        return Err(Error::PairTruncated {
                            pairs_read: self.pairs_read,
                            missing: "R2",
                        })
                    }
                    (false, true) => {
                        return Err(Error::PairTruncated {
                            pairs_read: self.pairs_read,
                            missing: "R1",
                        })
                    }
                }
            }
            Source::Interleaved(reader) => {
                if !reader.read_into(&mut pair.first)? {
                    return Ok(false);
                }
                if !reader.read_into(&mut pair.second)? {
                    return Err(Error::PairTruncated {
                        pairs_read: self.pairs_read,
                        missing: "the second mate",
                    });
                }
                true
            }
        };

        if self.check_names && !pair.names_match() {
            return Err(Error::PairMismatch {
                pairs_read: self.pairs_read,
                first: pair.first.id.clone(),
                second: pair.second.id.clone(),
            });
        }
        self.pairs_read += 1;
        Ok(present)
    }

    /// Read the next pair into a fresh [`Pair`].
    pub fn read_pair(&mut self) -> Result<Option<Pair>> {
        let mut pair = Pair::default();
        if self.read_into(&mut pair)? {
            Ok(Some(pair))
        } else {
            Ok(None)
        }
    }

    /// Run `f` on every remaining pair, reusing one buffer.
    pub fn for_each_pair<F>(&mut self, mut f: F) -> Result<()>
    where
        F: FnMut(&Pair) -> Result<()>,
    {
        let mut pair = Pair::default();
        while self.read_into(&mut pair)? {
            f(&pair)?;
        }
        Ok(())
    }

    /// Count the remaining pairs.
    pub fn count_pairs(&mut self) -> Result<u64> {
        let mut pairs = 0;
        let mut pair = Pair::default();
        while self.read_into(&mut pair)? {
            pairs += 1;
        }
        Ok(pairs)
    }
}

impl<R: Read> Iterator for PairedReader<R> {
    type Item = Result<Pair>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.read_pair() {
            Ok(Some(pair)) => Some(Ok(pair)),
            Ok(None) => None,
            Err(e) => Some(Err(e)),
        }
    }
}

/// A writer for paired reads, to two files or one interleaved stream.
pub enum PairedWriter<W: Write> {
    /// Separate R1 and R2 outputs.
    Split {
        /// Where forward reads go.
        first: FastxWriter<W>,
        /// Where reverse reads go.
        second: FastxWriter<W>,
    },
    /// One output with the mates alternating.
    Interleaved(FastxWriter<W>),
}

impl<W: Write> PairedWriter<W> {
    /// Write one pair.
    pub fn write_pair(&mut self, pair: &Pair) -> Result<()> {
        match self {
            PairedWriter::Split { first, second } => {
                first.write_record(&pair.first)?;
                second.write_record(&pair.second)?;
            }
            PairedWriter::Interleaved(writer) => {
                writer.write_record(&pair.first)?;
                writer.write_record(&pair.second)?;
            }
        }
        Ok(())
    }

    /// Flush every underlying writer.
    pub fn flush(&mut self) -> Result<()> {
        match self {
            PairedWriter::Split { first, second } => {
                first.flush()?;
                second.flush()?;
            }
            PairedWriter::Interleaved(writer) => writer.flush()?,
        }
        Ok(())
    }

    /// Flush and finish, surfacing any error from closing a compressed stream.
    pub fn finish(self) -> Result<()> {
        match self {
            PairedWriter::Split { first, second } => {
                first.finish()?;
                second.finish()?;
            }
            PairedWriter::Interleaved(writer) => {
                writer.finish()?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const R1: &[u8] = b"@read1/1\nACGT\n+\nIIII\n@read2/1\nTTTT\n+\nJJJJ\n";
    const R2: &[u8] = b"@read1/2\nCCCC\n+\nIIII\n@read2/2\nGGGG\n+\nJJJJ\n";

    #[test]
    fn reads_pairs_from_two_streams() {
        let mut reader = PairedReader::from_readers(R1, R2);
        let pairs: Vec<Pair> = reader.by_ref().collect::<Result<Vec<_>>>().unwrap();
        assert_eq!(pairs.len(), 2);
        assert_eq!(pairs[0].first.seq, b"ACGT");
        assert_eq!(pairs[0].second.seq, b"CCCC");
        assert_eq!(pairs[1].first.id, "read2/1");
        assert_eq!(reader.pairs_read(), 2);
    }

    #[test]
    fn reads_interleaved() {
        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";
        let pairs: Vec<Pair> = PairedReader::interleaved(&interleaved[..])
            .collect::<Result<Vec<_>>>()
            .unwrap();
        assert_eq!(pairs.len(), 2);
        assert_eq!(pairs[0].second.seq, b"GT");
        assert_eq!(pairs[1].first.id, "s/1");
    }

    #[test]
    fn catches_mispaired_names() {
        // R2 sorted differently: the classic silent pipeline bug.
        let shuffled = b"@read2/2\nGGGG\n+\nJJJJ\n@read1/2\nCCCC\n+\nIIII\n";
        let error = PairedReader::from_readers(R1, &shuffled[..])
            .read_pair()
            .unwrap_err();
        match error {
            Error::PairMismatch { first, second, .. } => {
                assert_eq!((first.as_str(), second.as_str()), ("read1/1", "read2/2"));
            }
            other => panic!("expected PairMismatch, got {other}"),
        }

        // The check can be waived for data with unconventional names.
        let pairs = PairedReader::from_readers(R1, &shuffled[..])
            .check_names(false)
            .count_pairs()
            .unwrap();
        assert_eq!(pairs, 2);
    }

    #[test]
    fn catches_a_truncated_mate_file() {
        let short = b"@read1/2\nCCCC\n+\nIIII\n";
        let error = PairedReader::from_readers(R1, &short[..])
            .count_pairs()
            .unwrap_err();
        assert!(
            matches!(
                error,
                Error::PairTruncated {
                    missing: "R2",
                    pairs_read: 1
                }
            ),
            "{error}"
        );

        let error = PairedReader::from_readers(&short[..], R2)
            .count_pairs()
            .unwrap_err();
        assert!(
            matches!(error, Error::PairTruncated { missing: "R1", .. }),
            "{error}"
        );

        // An odd number of interleaved records is the same problem.
        let odd = b"@r/1\nAC\n+\nII\n@r/2\nGT\n+\nII\n@s/1\nAA\n+\nII\n";
        let error = PairedReader::interleaved(&odd[..])
            .count_pairs()
            .unwrap_err();
        assert!(
            matches!(error, Error::PairTruncated { pairs_read: 1, .. }),
            "{error}"
        );
    }

    #[test]
    fn accepts_illumina_style_names() {
        // Modern Illumina puts the mate number after a space, so the ids match
        // outright once the header is split.
        let r1 = b"@A00123:1:HXX:1:1101:1000:1000 1:N:0:ATCG\nAC\n+\nII\n";
        let r2 = b"@A00123:1:HXX:1:1101:1000:1000 2:N:0:ATCG\nGT\n+\nII\n";
        let pair = PairedReader::from_readers(&r1[..], &r2[..])
            .read_pair()
            .unwrap()
            .unwrap();
        assert!(pair.names_match());
        assert_eq!(pair.first.description.as_deref(), Some("1:N:0:ATCG"));
    }

    #[test]
    fn mate_stems() {
        for (id, stem) in [
            ("read/1", "read"),
            ("read/2", "read"),
            ("read.1", "read"),
            ("read_2", "read"),
            ("read", "read"),
            ("read1", "read1"),
            ("r", "r"),
            ("", ""),
            ("/1", ""),
            ("read/3", "read/3"),
        ] {
            assert_eq!(mate_stem(id), stem, "{id}");
        }
    }

    #[test]
    fn empty_input_yields_no_pairs() {
        assert_eq!(
            PairedReader::from_readers(&b""[..], &b""[..])
                .count_pairs()
                .unwrap(),
            0
        );
        assert_eq!(
            PairedReader::interleaved(&b""[..]).count_pairs().unwrap(),
            0
        );
    }

    #[test]
    fn round_trips_through_the_writer() {
        let mut interleaved = Vec::new();
        {
            let mut writer =
                PairedWriter::Interleaved(FastxWriter::new(&mut interleaved, Format::Fastq));
            let mut reader = PairedReader::from_readers(R1, R2);
            reader
                .for_each_pair(|pair| writer.write_pair(pair))
                .unwrap();
            writer.finish().unwrap();
        }

        let pairs: Vec<Pair> = PairedReader::interleaved(&interleaved[..])
            .collect::<Result<Vec<_>>>()
            .unwrap();
        let original: Vec<Pair> = PairedReader::from_readers(R1, R2)
            .collect::<Result<Vec<_>>>()
            .unwrap();
        assert_eq!(pairs, original);
    }

    #[test]
    fn split_writer_separates_the_mates() {
        let mut first = Vec::new();
        let mut second = Vec::new();
        {
            let mut writer = PairedWriter::Split {
                first: FastxWriter::new(&mut first, Format::Fastq),
                second: FastxWriter::new(&mut second, Format::Fastq),
            };
            PairedReader::from_readers(R1, R2)
                .for_each_pair(|pair| writer.write_pair(pair))
                .unwrap();
            writer.finish().unwrap();
        }
        assert_eq!(first, R1);
        assert_eq!(second, R2);
    }
}