thermo_parity/
thermo_parity.rs1use std::collections::BTreeMap;
15use std::path::Path;
16use std::time::Instant;
17
18use koth_ms::config::FileConfig;
19use koth_ms::io;
20use koth_ms::models::Spectrum;
21
22fn main() -> anyhow::Result<()> {
23 let args: Vec<String> = std::env::args().collect();
24 match args.get(1).map(String::as_str) {
25 Some("ms1") => ms1(Path::new(&args[2]), Path::new(&args[3])),
26 Some("ms2") => ms2(Path::new(&args[2]), Path::new(&args[3])),
27 Some("load") => load(Path::new(&args[2])),
28 _ => anyhow::bail!("usage: thermo_parity ms1|ms2 RAW MZML | load PATH"),
29 }
30}
31
32fn load(path: &Path) -> anyhow::Result<()> {
33 let t = Instant::now();
34 let mut n = 0usize;
35 let mut peaks = 0usize;
36 for s in io::stream_spectra(path, &FileConfig::default())? {
37 let s = s?;
38 n += 1;
39 peaks += s.peaks.len();
40 }
41 println!(
42 "load {}: {n} MS1 spectra, {peaks} peaks, {:.2} s",
43 path.display(),
44 t.elapsed().as_secs_f64()
45 );
46 Ok(())
47}
48
49fn read_timed(path: &Path) -> anyhow::Result<Vec<Spectrum>> {
50 let t = Instant::now();
51 let spectra = io::read_spectra(path, &FileConfig::default())?;
52 println!(
53 "read {:>6} MS1 spectra in {:6.2} s from {}",
54 spectra.len(),
55 t.elapsed().as_secs_f64(),
56 path.display()
57 );
58 Ok(spectra)
59}
60
61fn ms1(raw: &Path, mzml: &Path) -> anyhow::Result<()> {
62 let a = read_timed(raw)?;
63 let b = read_timed(mzml)?;
64 println!("MS1 spectra: raw {} mzML {}", a.len(), b.len());
65 let n = a.len().min(b.len());
66 let mut max_rt = 0.0f64;
67 let mut same_count = 0usize;
68 let (mut peaks_a, mut peaks_b) = (0usize, 0usize);
69 let mut max_ppm = 0.0f64;
70 let mut max_rel_int = 0.0f64;
71 let mut compared = 0usize;
72 let mut faims_mismatch = 0usize;
73 for (sa, sb) in a.iter().zip(b.iter()).take(n) {
74 max_rt = max_rt.max((sa.retention_time - sb.retention_time).abs());
75 peaks_a += sa.peaks.len();
76 peaks_b += sb.peaks.len();
77 if sa.faims_cv != sb.faims_cv {
78 faims_mismatch += 1;
79 }
80 if sa.peaks.len() != sb.peaks.len() {
81 continue;
82 }
83 same_count += 1;
84 for (pa, pb) in sa.peaks.iter().zip(&sb.peaks) {
85 let ppm = ((pa.mz - pb.mz) as f64).abs() / pb.mz as f64 * 1e6;
86 max_ppm = max_ppm.max(ppm);
87 let rel =
88 ((pa.intensity - pb.intensity) as f64).abs() / (pb.intensity as f64).max(1e-12);
89 max_rel_int = max_rel_int.max(rel);
90 compared += 1;
91 }
92 }
93 println!("paired {n}; max |dRT| = {:.3e} min", max_rt);
94 println!(
95 "identical peak count in {same_count}/{n} scans; total peaks raw {peaks_a} mzML {peaks_b}"
96 );
97 println!(
98 "over {compared} paired peaks: max |dm/z| = {max_ppm:.4} ppm, max rel |dI| = {max_rel_int:.3e}"
99 );
100 println!("FAIMS CV mismatches: {faims_mismatch}");
101 Ok(())
102}
103
104fn ms2(raw: &Path, mzml: &Path) -> anyhow::Result<()> {
105 let t = Instant::now();
106 let a = io::thermo::read_thermo_ms2(raw)?;
107 println!(
108 "raw: {} MS2 spectra in {:.2} s",
109 a.len(),
110 t.elapsed().as_secs_f64()
111 );
112 let b: Vec<Spectrum> = io::mzml::stream_mzml_ms2(mzml)?.collect();
113 println!("mzML: {} MS2 spectra", b.len());
114 let windows = |v: &[Spectrum]| {
115 let mut m: BTreeMap<(i64, i64, i64), usize> = BTreeMap::new();
116 for s in v {
117 *m.entry(s.isolation_window.unwrap().key()).or_default() += 1;
118 }
119 m
120 };
121 let (wa, wb) = (windows(&a), windows(&b));
122 println!("distinct windows: raw {} mzML {}", wa.len(), wb.len());
123 let fmt = |k: &(i64, i64, i64)| {
124 format!(
125 "{:.3} [{:.3}, {:.3}]",
126 k.0 as f64 / 1e4,
127 k.1 as f64 / 1e4,
128 k.2 as f64 / 1e4
129 )
130 };
131 for (k, c) in wa.iter().take(5) {
132 println!(" raw {} x{c}", fmt(k));
133 }
134 for (k, c) in wb.iter().take(5) {
135 println!(" mzML {} x{c}", fmt(k));
136 }
137 let n = a.len().min(b.len());
139 let (mut same_w, mut same_n, mut max_rt, mut max_bound) = (0usize, 0usize, 0.0f64, 0.0f64);
140 for (sa, sb) in a.iter().zip(&b).take(n) {
141 let (wa, wb) = (sa.isolation_window.unwrap(), sb.isolation_window.unwrap());
142 max_rt = max_rt.max((sa.retention_time - sb.retention_time).abs());
143 let d = (wa.target - wb.target)
144 .abs()
145 .max((wa.lower - wb.lower).abs())
146 .max((wa.upper - wb.upper).abs());
147 max_bound = max_bound.max(d);
148 if wa.key() == wb.key() {
149 same_w += 1;
150 }
151 if sa.peaks.len() == sb.peaks.len() {
152 same_n += 1;
153 }
154 }
155 println!(
156 "paired {n}: identical window key {same_w}, identical peak count {same_n}, \
157 max |dRT| {max_rt:.3e} min, max window bound diff {max_bound:.4} Th"
158 );
159 Ok(())
160}