Skip to main content

illumina_coordinates/
lib.rs

1//! # illumina_coordinates
2//!
3//! This crate provides a single function to parse sequence identifiers from FASTQ files created
4//! by Illumina sequencers. Sequence identifiers contain information about each read, including
5//! the physical location of the DNA cluster on the flow cell surface that contained the
6//! associated sequence.
7//!
8//! Illumina was not involved in the creation of this library in any way.
9
10#![crate_type="lib"]
11#![deny(warnings, missing_docs)]
12use std::convert::From;
13use std::result::Result;
14use std::num;
15
16
17#[derive(Debug, PartialOrd, PartialEq)]
18/// Sample numbers are either the number from the sample sheet or a sequence if the read was from
19/// the Undetermined Reads
20pub enum Sample {
21    /// Sample number
22    Number(u8),
23    /// Sequence from Undetermined Reads
24    Sequence(String)
25}
26
27/// A parsed sequence identifier
28pub struct SequenceIdentifier {
29    /// ID of the sequencing machine
30    pub sequencer_id: String,
31    /// The number of sequencing runs this machine has performed
32    pub run_count: u16,
33    /// ID of the flow cell, printed on the side of the glass slide
34    pub flow_cell_id: String,
35    /// Lane number. For MiSeqs, this is always 1
36    pub lane: u8,
37    /// The near or far side off the flow cell surface
38    pub side: u8,
39    /// The row within a lane, if wide enough. For MiSeqs, this is always 1
40    pub swath: u8,
41    /// The positional order of the region where the cluster is located
42    pub tile: u8,
43    /// The x-coordinate of the cluster
44    pub x: u16,
45    /// The y-coordinate of the cluster
46    pub y: u16,
47    /// The read number
48    pub read: u8,
49    /// Whether the read was filtered for low quality (Y=filtered)
50    pub is_filtered: bool,
51    /// Indicates the type of control, 0 = not a control read
52    pub control_number: u8,
53    /// Number from sample sheet, or the sequence if the read is in Undetermined Reads
54    pub sample: Sample
55}
56
57#[derive(Debug)]
58/// Errors encountered when parsing FASTQ files
59pub enum IlluminaError {
60    /// We expected an integer but did not find one
61    ParseError,
62    /// The line was not structured as expected
63    SplitError
64}
65
66impl From<num::ParseIntError> for IlluminaError {
67    fn from(_: num::ParseIntError) -> IlluminaError {
68        IlluminaError::ParseError
69    }
70}
71
72/// Parses location information from an Illumina sequence identifier. This implementation is
73/// about 3x faster than using a regular expression.
74///
75/// The fields in the example identifier below have the following meaning:
76/// @M03745:11:000000000-B54L5:1:2108:4127:8949 1:N:0:0
77///
78/// M03745              ID of the sequencing machine
79///
80/// 11                  run count for this machine
81///
82/// 000000000-B54L5     ID of the flow cell. "B54L5" will be printed on the flow cell in this example
83///
84/// 1                   lane number. For MiSeqs, there's only one lane
85///
86/// 2108                the first digit is the side of the chip
87///                     the second digit is the swath. For MiSeqs, this is always 1. For HiSeqs, each lane is two tiles
88///                     wide, and the first pass from left-to-right is swath one, then the returning pass on the other
89///                     side of the lane is swath two
90///                     the last two digits are the order of the tile. For MiSeqs, this is a number from 1 to 19
91///
92/// 4127                the x-position of the read in the tile, in arbitrary units
93///
94/// 8949                the y-position of the read in the tile, in arbitrary units
95///
96/// 1                   First (forward) read in a paired-end run
97///
98/// N                   Read was not filtered (sufficient quality)
99///
100/// 0                   This was not a control
101///
102/// 0                   This was the first sample on the sample sheet
103///
104/// See https://help.basespace.illumina.com/articles/descriptive/fastq-files/ for more information.
105///
106/// # Example
107///
108/// ```rust
109/// extern crate illumina_coordinates;
110/// use illumina_coordinates::Sample;
111///
112/// fn main() {
113///     let line = "@M03745:11:000000000-B54L5:1:2108:4127:8949 1:N:0:0";
114///     let seq_id = illumina_coordinates::parse_sequence_identifier(&line).unwrap();
115///     assert_eq!(seq_id.sequencer_id, "M03745".to_string());
116///     assert_eq!(seq_id.run_count, 11);
117///     assert_eq!(seq_id.flow_cell_id, "000000000-B54L5".to_string());
118///     assert_eq!(seq_id.lane, 1);
119///     assert_eq!(seq_id.side, 2);
120///     assert_eq!(seq_id.swath, 1);
121///     assert_eq!(seq_id.tile, 8);
122///     assert_eq!(seq_id.x, 4127);
123///     assert_eq!(seq_id.y, 8949);
124///     assert_eq!(seq_id.read, 1);
125///     assert_eq!(seq_id.is_filtered, false);
126///     assert_eq!(seq_id.control_number, 0);
127///     assert_eq!(seq_id.sample, Sample::Number(0));
128/// }
129/// ```
130pub fn parse_sequence_identifier(text: &str) -> Result<SequenceIdentifier, IlluminaError> {
131    let halves: Vec<&str> = text.trim().split(' ').collect();
132    if halves.len() != 2 {
133        return Err(IlluminaError::SplitError)
134    }
135    let left: Vec<&str> = halves[0].split(':').collect();
136    let right: Vec<&str> = halves[1].split(':').collect();
137    if left.len() != 7 {
138        return Err(IlluminaError::SplitError);
139    }
140    if right.len() != 4 {
141        return Err(IlluminaError::SplitError);
142    }
143    let sequencer_id = left[0].split_at(1).1.to_string();
144    let run_count = left[1].parse::<u16>()?;
145    let flow_cell_id = left[2].to_string();
146    let lane = left[3].parse::<u8>()?;
147    let (side, remainder) = left[4].split_at(1);
148    let (swath, tile) = remainder.split_at(1);
149    let side = side.parse::<u8>()?;
150    let swath = swath.parse::<u8>()?;
151    let tile = tile.parse::<u8>()?;
152    let x = left[5].parse::<u16>()?;
153    let y = left[6].parse::<u16>()?;
154
155    let read = right[0].parse::<u8>()?;
156    let is_filtered = match right[1] {
157        "Y" => true,
158        "N" => false,
159        _ => return Err(IlluminaError::ParseError)
160    };
161    let control_number= right[2].parse::<u8>()?;
162    let sample = right[3].parse::<u8>();
163    let sample = match sample {
164        Ok(n) => Sample::Number(n),
165        Err(_) => Sample::Sequence(String::from(right[3]))
166    };
167
168    Ok(SequenceIdentifier {
169        sequencer_id,
170        run_count,
171        flow_cell_id,
172        lane,
173        side,
174        swath,
175        tile,
176        x,
177        y,
178        read,
179        is_filtered,
180        control_number,
181        sample
182    })
183}
184
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_parse() {
192        let line = "@M03745:11:000000000-B54L5:1:2108:4127:8949 1:N:0:0";
193        let seq_id = parse_sequence_identifier(&line).unwrap();
194        assert_eq!(seq_id.sequencer_id, "M03745".to_string());
195        assert_eq!(seq_id.run_count, 11);
196        assert_eq!(seq_id.flow_cell_id, "000000000-B54L5".to_string());
197        assert_eq!(seq_id.lane, 1);
198        assert_eq!(seq_id.side, 2);
199        assert_eq!(seq_id.swath, 1);
200        assert_eq!(seq_id.tile, 8);
201        assert_eq!(seq_id.x, 4127);
202        assert_eq!(seq_id.y, 8949);
203        assert_eq!(seq_id.read, 1);
204        assert_eq!(seq_id.is_filtered, false);
205        assert_eq!(seq_id.control_number, 0);
206        assert_eq!(seq_id.sample, Sample::Number(0));
207    }
208    
209    #[test]
210    fn test_parse_with_newline() {
211        let line = "@M03745:11:000000000-B54L5:1:2108:4127:8949 1:Y:0:0\n";
212        let seq_id = parse_sequence_identifier(&line).unwrap();
213        assert_eq!(seq_id.sequencer_id, "M03745".to_string());
214        assert_eq!(seq_id.run_count, 11);
215        assert_eq!(seq_id.flow_cell_id, "000000000-B54L5".to_string());
216        assert_eq!(seq_id.lane, 1);
217        assert_eq!(seq_id.side, 2);
218        assert_eq!(seq_id.swath, 1);
219        assert_eq!(seq_id.tile, 8);
220        assert_eq!(seq_id.x, 4127);
221        assert_eq!(seq_id.y, 8949);
222        assert_eq!(seq_id.read, 1);
223        assert_eq!(seq_id.is_filtered, true);
224        assert_eq!(seq_id.control_number, 0);
225        assert_eq!(seq_id.sample, Sample::Number(0));
226    }
227
228    #[test]
229    fn test_parse_error() {
230        let result = parse_sequence_identifier("CACGACGACTAGCTACGGACGCGGCACGACGCAG");
231        assert!(result.is_err());
232    }
233}