1use std::collections::HashMap;
2use std::env;
3use std::io;
4use std::path;
5use std::process;
6use std::thread::spawn;
7use std::time;
8
9use std::sync::mpsc::sync_channel;
10
11use mzdata::io::Source;
12use mzdata::meta::SpectrumType;
13use mzdata::prelude::*;
14use mzdata::spectrum::ChromatogramType;
15use mzdata::spectrum::{
16 DeconvolutedSpectrum, MultiLayerSpectrum, RefPeakDataLevel, SignalContinuity, SpectrumLike,
17};
18use mzdata::MZReader;
19
20struct MSDataFileSummary {
21 pub start_time: f64,
22 pub end_time: f64,
23 pub level_table: HashMap<u8, usize>,
24 pub charge_table: HashMap<i32, usize>,
25 pub peak_charge_table: HashMap<u8, HashMap<i32, usize>>,
26 pub peak_mode_table: HashMap<SignalContinuity, usize>,
27 pub has_ion_mobility: bool,
28 pub spectrum_types: HashMap<Option<SpectrumType>, usize>,
29 pub chromatogram_types: HashMap<ChromatogramType, usize>,
30}
31
32impl Default for MSDataFileSummary {
33 fn default() -> Self {
34 Self {
35 start_time: f64::INFINITY,
36 end_time: f64::NEG_INFINITY,
37 level_table: Default::default(),
38 charge_table: Default::default(),
39 peak_charge_table: Default::default(),
40 peak_mode_table: Default::default(),
41 has_ion_mobility: false,
42 spectrum_types: Default::default(),
43 chromatogram_types: Default::default(),
44 }
45 }
46}
47
48impl MSDataFileSummary {
49 pub fn handle_scan(&mut self, scan: MultiLayerSpectrum) {
50 let time = scan.start_time();
51 self.start_time = self.start_time.min(time);
52 self.end_time = self.end_time.max(time);
53 let level = scan.ms_level();
54 *self.level_table.entry(level).or_default() += 1;
55 *self.spectrum_types.entry(scan.spectrum_type()).or_default() += 1;
56 if level > 1 {
57 if let Some(charge) = scan.precursor().unwrap().ion().and_then(|i| i.charge()) {
58 *self.charge_table.entry(charge).or_default() += 1;
59 } else {
60 *self.charge_table.entry(0).or_default() += 1;
61 }
62 }
63 *self
64 .peak_mode_table
65 .entry(scan.signal_continuity())
66 .or_default() += scan.peaks().len();
67
68 let has_charge = match scan.peaks() {
69 RefPeakDataLevel::Missing => false,
70 RefPeakDataLevel::RawData(arrays) => arrays.charges().is_ok(),
71 RefPeakDataLevel::Centroid(_) => false,
72 RefPeakDataLevel::Deconvoluted(_) => true,
73 };
74
75 let has_ion_mobility = match scan.peaks() {
76 RefPeakDataLevel::RawData(arrays) => arrays.has_ion_mobility(),
77 _ => false,
78 } || scan.has_ion_mobility();
79 self.has_ion_mobility |= has_ion_mobility;
80
81 if has_charge {
82 let deconv_scan: DeconvolutedSpectrum = scan.try_into().unwrap();
83 deconv_scan.deconvoluted_peaks.iter().for_each(|p| {
84 *(*self
85 .peak_charge_table
86 .entry(deconv_scan.ms_level())
87 .or_default())
88 .entry(p.charge)
89 .or_default() += 1;
90 assert!((p.index as usize) < deconv_scan.deconvoluted_peaks.len())
91 })
92 }
93 }
94
95 pub fn _scan_file<R: SpectrumSource>(&mut self, reader: &mut R) {
96 let start = time::Instant::now();
97 reader.enumerate().for_each(|(i, scan)| {
98 if i % 10000 == 0 && i > 0 {
99 println!(
100 "\tScan {}: {} ({:0.3} seconds, {} peaks|points)",
101 i,
102 scan.id(),
103 (time::Instant::now() - start).as_secs_f64(),
104 self.peak_mode_table.values().sum::<usize>()
105 );
106 }
107 self.handle_scan(scan);
108 });
109 let end = time::Instant::now();
110 let elapsed = end - start;
111 println!("{:0.3} seconds elapsed", elapsed.as_secs_f64());
112 }
113
114 pub fn scan_file<R: SpectrumSource + Send + 'static>(&mut self, reader: R) {
115 self.scan_file_threaded(reader)
116 }
117
118 pub fn scan_file_threaded<R: SpectrumSource + Send + 'static>(&mut self, reader: R) {
119 let start = time::Instant::now();
120 let (sender, receiver) = sync_channel(2usize.pow(12));
121 let read_handle = spawn(move || {
122 reader
123 .into_iter()
124 .enumerate()
125 .for_each(|(i, scan)| sender.send((i, scan)).unwrap());
126 });
127 let i = receiver.iter().fold(0, |_, (i, scan)| {
128 if i % 10000 == 0 && i > 0 {
129 println!(
130 "\tScan {}: {} ({:0.3} seconds, {} peaks|points)",
131 i,
132 scan.id(),
133 (time::Instant::now() - start).as_secs_f64(),
134 self.peak_mode_table.values().sum::<usize>()
135 );
136 }
137 self.handle_scan(scan);
138 i
139 });
140 read_handle.join().unwrap();
141 let end = time::Instant::now();
142 let elapsed = end - start;
143 println!(
144 "{:0.3} seconds elapsed, handled {i} spectra",
145 elapsed.as_secs_f64()
146 );
147 }
148
149 pub fn scan_chromatograms<R: ChromatogramSource + Send + 'static>(&mut self, reader: &mut R) {
150 for chrom in reader.iter_chromatograms() {
151 *self
152 .chromatogram_types
153 .entry(chrom.chromatogram_type())
154 .or_default() += 1;
155 }
156 }
157
158 pub fn write_out(&self) {
159 println!("Start Time: {:0.2}", self.start_time);
160 println!("End Time: {:0.2}", self.end_time);
161 println!("Has Ion Mobility: {}", self.has_ion_mobility);
162 println!("MS Levels:");
163 let mut level_set: Vec<(&u8, &usize)> = self.level_table.iter().collect();
164 level_set.sort_by_key(|(a, _)| *a);
165 for (level, count) in level_set.iter() {
166 println!("\t{}: {}", level, count);
167 }
168
169 let mut spec_types: Vec<(_, _)> = self.spectrum_types.iter().collect();
170 spec_types.sort_by(|a, b| a.0.cmp(b.0));
171
172 if spec_types.iter().any(|v| {
173 v.0.is_some_and(|v| v != SpectrumType::MS1Spectrum && v != SpectrumType::MSnSpectrum)
174 }) {
175 println!("Spectrum types:");
176 for (level, count) in spec_types.iter() {
177 if let Some(level) = level {
178 println!("\t{}: {}", level.name(), count);
179 } else {
180 println!("\tUnknown: {}", count);
181 }
182 }
183 }
184
185 println!("Precursor Charge States:");
186 let mut charge_set: Vec<(&i32, &usize)> = self.charge_table.iter().collect();
187 charge_set.sort_by_key(|(a, _)| *a);
188 for (charge, count) in charge_set.iter() {
189 if **charge == 0 {
190 println!("\tCharge Not Reported: {}", count);
191 } else {
192 println!("\t{}: {}", charge, count);
193 }
194 }
195
196 let mut peak_charge_levels: Vec<_> = self.peak_charge_table.iter().collect();
197
198 peak_charge_levels.sort_by(|(level_a, _), (level_b, _)| level_a.cmp(level_b));
199
200 for (level, peak_charge_table) in peak_charge_levels {
201 if !peak_charge_table.is_empty() {
202 println!("Peak Charge States for MS level {}:", level);
203 let mut peak_charge_set: Vec<(&i32, &usize)> = peak_charge_table.iter().collect();
204 peak_charge_set.sort_by_key(|(a, _)| *a);
205 for (charge, count) in peak_charge_set.iter() {
206 if **charge == 0 {
207 println!("\tCharge Not Reported: {}", count);
208 } else {
209 println!("\t{}: {}", charge, count);
210 }
211 }
212 }
213 }
214 self.peak_mode_table
215 .iter()
216 .for_each(|(mode, count)| match mode {
217 SignalContinuity::Unknown => println!("Unknown continuity: {}", count),
218 SignalContinuity::Centroid => println!("Peaks: {}", count),
219 SignalContinuity::Profile => println!("Points: {}", count),
220 });
221
222 if !self.chromatogram_types.is_empty() {
223 let mut spec_types: Vec<(_, _)> = self.chromatogram_types.iter().collect();
224 spec_types.sort_by(|a, b| a.0.cmp(b.0));
225 println!("Chromatogram types:");
226 for (level, count) in spec_types.iter() {
227 println!("\t{:?}: {}", level, count);
228 }
229 }
230 }
231}
232
233fn main() -> io::Result<()> {
234 env_logger::init();
235 let path = path::PathBuf::from(env::args().nth(1).unwrap_or_else(|| {
236 eprintln!("Please provide a path to an MS data file");
237 process::exit(1)
238 }));
239 let mut summarizer = MSDataFileSummary::default();
240
241 if path.as_os_str() == "-" {
242 mzdata::mz_read!(Source::Stdin, reader => {
243 summarizer.scan_file(reader)
244 })?;
245 } else {
246 let mut reader = MZReader::open_path(path)?;
247 eprintln!("Format: {}", reader.as_format());
248 if reader.count_chromatograms() > 0 {
249 summarizer.scan_chromatograms(&mut reader);
250 reader.reset();
251 }
252 summarizer.scan_file(reader)
253 };
254
255 summarizer.write_out();
256 Ok(())
257}