Skip to main content

emit_sample_mzml/
emit_sample_mzml.rs

1//! Emit a small synthetic mzML document (plain and indexed) from the
2//! canonical writer, for schema-conformance checking in CI.
3//!
4//! Usage:
5//!   emit_sample_mzml [PLAIN_OUT] [INDEXED_OUT]
6//!
7//! Defaults to `sample_plain.mzML` and `sample_indexed.mzML` in the
8//! current directory. The output is validated against the vendored
9//! PSI-MS mzML XSD by the `validate-mzml` CI job.
10
11use std::fs::File;
12use std::io::BufWriter;
13
14use openmassspec_core::{
15    write_indexed_mzml, write_mzml, Activation, Analyzer, ChromatogramRecord, CvTerm,
16    MobilityArrayKind, MsPower, Polarity, PrecursorInfo, RunMetadata, ScanMode, SpectrumRecord,
17    SpectrumSource,
18};
19
20struct SampleSource {
21    meta: RunMetadata,
22    spectra: Vec<SpectrumRecord>,
23    chroms: Vec<ChromatogramRecord>,
24    cursor: usize,
25}
26
27impl SampleSource {
28    fn new() -> Self {
29        let meta = RunMetadata {
30            source_file_name: "sample.raw".into(),
31            source_file_format: CvTerm::new("MS:1000563", "Thermo RAW format"),
32            native_id_format: CvTerm::new("MS:1000768", "Thermo nativeID format"),
33            instrument: CvTerm::new("MS:1001911", "Q Exactive"),
34            instrument_serial_number: Some("SN-EXAMPLE-001".into()),
35            software_name: "openmassspec-core-sample".into(),
36            software_version: env!("CARGO_PKG_VERSION").into(),
37            // Exercises the `startTimeStamp` attribute path so it is
38            // schema-checked without a vendor file.
39            start_timestamp: Some("2026-06-01T14:30:00Z".into()),
40            mobility_array_kind: Some(MobilityArrayKind::InverseReducedVsPerCm2),
41            // Exercises the per-spectrum `instrumentConfigurationRef` path:
42            // the FTMS (Orbitrap) spectra below get one instrumentConfiguration
43            // and the TOFMS one gets another, so it is schema-checked without
44            // a vendor file.
45            analyzers: vec![Analyzer::FTMS, Analyzer::TOFMS],
46        };
47        let ms1 = SpectrumRecord {
48            index: 0,
49            scan_number: 1,
50            native_id: "controllerType=0 controllerNumber=1 scan=1".into(),
51            ms_level: MsPower::Ms1.ms_level(),
52            polarity: Some(Polarity::Positive),
53            scan_mode: Some(ScanMode::Centroid),
54            analyzer: Some(Analyzer::FTMS),
55            filter: Some("FTMS + p ESI Full ms".into()),
56            retention_time_sec: 0.123 * 60.0,
57            total_ion_current: None,
58            base_peak_mz: None,
59            base_peak_intensity: None,
60            low_mz: None,
61            high_mz: None,
62            ion_injection_time_ms: Some(20.0),
63            inv_mobility: None,
64            faims_cv: None,
65            precursor: None,
66            mz: vec![100.0, 200.0, 300.0],
67            intensity: vec![1.0, 5.0, 2.0],
68            inv_mobility_per_peak: None,
69        };
70        let ms2 = SpectrumRecord {
71            index: 1,
72            scan_number: 2,
73            native_id: "controllerType=0 controllerNumber=1 scan=2".into(),
74            ms_level: MsPower::Ms2.ms_level(),
75            polarity: Some(Polarity::Positive),
76            scan_mode: Some(ScanMode::Centroid),
77            analyzer: Some(Analyzer::FTMS),
78            filter: Some("FTMS + p ESI d Full ms2 200.00@hcd28.00".into()),
79            retention_time_sec: 0.5 * 60.0,
80            total_ion_current: Some(123.45),
81            base_peak_mz: Some(150.5),
82            base_peak_intensity: Some(99.0),
83            low_mz: Some(100.0),
84            high_mz: Some(180.0),
85            ion_injection_time_ms: Some(50.0),
86            inv_mobility: None,
87            faims_cv: Some(-45.0),
88            precursor: Some(PrecursorInfo {
89                target_mz: Some(200.0),
90                selected_mz: Some(200.001),
91                isolation_width: Some(2.0),
92                charge: Some(2),
93                intensity: None,
94                collision_energy: Some(28.0),
95                ce_is_nce: true,
96                precursor_native_id: Some("controllerType=0 controllerNumber=1 scan=1".into()),
97                activation: Some(Activation::CID),
98                analyzer: Some(Analyzer::FTMS),
99                // Exercises the `collisional cross sectional area` selectedIon
100                // cvParam path so it is schema-checked without a vendor file.
101                ccs: Some(153.4),
102            }),
103            mz: vec![150.5, 160.0],
104            intensity: vec![99.0, 50.0],
105            inv_mobility_per_peak: None,
106        };
107        // A frame-collapsed ion-mobility MS1 (the Bruker timsTOF shape):
108        // scalar 1/K0 plus a per-peak inverse-reduced-mobility array, which
109        // the writer emits as a third binary data array. Exercises the
110        // ion-mobility CV path so it is schema-checked without a vendor file.
111        let ms1_mobility = SpectrumRecord {
112            index: 2,
113            scan_number: 3,
114            native_id: "frame=1 scan=0".into(),
115            ms_level: MsPower::Ms1.ms_level(),
116            polarity: Some(Polarity::Positive),
117            scan_mode: Some(ScanMode::Centroid),
118            analyzer: Some(Analyzer::TOFMS),
119            filter: None,
120            retention_time_sec: 0.75 * 60.0,
121            total_ion_current: None,
122            base_peak_mz: None,
123            base_peak_intensity: None,
124            low_mz: None,
125            high_mz: None,
126            ion_injection_time_ms: None,
127            inv_mobility: Some(0.95),
128            faims_cv: None,
129            precursor: None,
130            mz: vec![120.0, 240.0, 360.0],
131            intensity: vec![3.0, 7.0, 4.0],
132            inv_mobility_per_peak: Some(vec![0.92, 0.95, 0.98]),
133        };
134        // A TIC and a SRM/MRM transition chromatogram. Exercises the
135        // `chromatogramList` path (plain + indexed) so it is schema-checked
136        // without a vendor file.
137        let tic = ChromatogramRecord {
138            index: 0,
139            id: "TIC".into(),
140            chromatogram_type: Some(CvTerm::new("MS:1000235", "total ion current chromatogram")),
141            precursor_mz: None,
142            product_mz: None,
143            time_sec: vec![0.0, 30.0, 45.0],
144            intensity: vec![120.0, 340.0, 210.0],
145        };
146        let srm = ChromatogramRecord {
147            index: 1,
148            id: "SRM SIC Q1=524.3 Q3=136.1".into(),
149            chromatogram_type: Some(CvTerm::new(
150                "MS:1001473",
151                "selected reaction monitoring chromatogram",
152            )),
153            precursor_mz: Some(524.3),
154            product_mz: Some(136.1),
155            time_sec: vec![0.0, 30.0],
156            intensity: vec![50.0, 80.0],
157        };
158        Self {
159            meta,
160            spectra: vec![ms1, ms2, ms1_mobility],
161            chroms: vec![tic, srm],
162            cursor: 0,
163        }
164    }
165}
166
167impl SpectrumSource for SampleSource {
168    fn run_metadata(&self) -> RunMetadata {
169        self.meta.clone()
170    }
171
172    fn iter_spectra<'a>(&'a mut self) -> Box<dyn Iterator<Item = SpectrumRecord> + 'a> {
173        self.cursor = 0;
174        Box::new(std::iter::from_fn(move || {
175            let rec = self.spectra.get(self.cursor).cloned();
176            if rec.is_some() {
177                self.cursor += 1;
178            }
179            rec
180        }))
181    }
182
183    fn iter_chromatograms<'a>(&'a mut self) -> Box<dyn Iterator<Item = ChromatogramRecord> + 'a> {
184        Box::new(self.chroms.clone().into_iter())
185    }
186
187    fn spectrum_count_hint(&self) -> Option<usize> {
188        Some(self.spectra.len())
189    }
190}
191
192fn main() -> std::io::Result<()> {
193    let mut args = std::env::args().skip(1);
194    let plain_path = args.next().unwrap_or_else(|| "sample_plain.mzML".into());
195    let indexed_path = args.next().unwrap_or_else(|| "sample_indexed.mzML".into());
196
197    let mut src = SampleSource::new();
198    let mut plain = BufWriter::new(File::create(&plain_path)?);
199    write_mzml(&mut src, &mut plain)
200        .map_err(|e| std::io::Error::other(format!("write_mzml: {e}")))?;
201
202    let mut indexed = BufWriter::new(File::create(&indexed_path)?);
203    write_indexed_mzml(&mut src, &mut indexed)
204        .map_err(|e| std::io::Error::other(format!("write_indexed_mzml: {e}")))?;
205
206    eprintln!("wrote {plain_path} and {indexed_path}");
207    Ok(())
208}