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}