Skip to main content

mzinfo/
mzinfo.rs

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}