1use std::sync::Arc;
22
23use arrow_array::builder::{
24 ArrayBuilder, Float32Builder, Float64Builder, Int32Builder, LargeListBuilder, StringBuilder,
25 UInt32Builder, UInt8Builder,
26};
27use arrow_array::{ArrayRef, RecordBatch};
28use arrow_schema::{DataType, Field, Schema, SchemaRef};
29
30use crate::{Activation, Analyzer, MobilityArrayKind, Polarity, ScanMode, SpectrumRecord};
31
32pub fn spectrum_record_schema() -> SchemaRef {
34 let mz_item = Arc::new(Field::new("item", DataType::Float64, false));
35 let int_item = Arc::new(Field::new("item", DataType::Float32, false));
36 let mob_item = Arc::new(Field::new("item", DataType::Float32, false));
37 Arc::new(Schema::new(vec![
38 Field::new("index", DataType::UInt32, false),
39 Field::new("scan_number", DataType::UInt32, false),
40 Field::new("native_id", DataType::Utf8, false),
41 Field::new("ms_level", DataType::UInt8, false),
42 Field::new("polarity", DataType::Utf8, true),
43 Field::new("scan_mode", DataType::Utf8, true),
44 Field::new("analyzer", DataType::Utf8, true),
45 Field::new("filter", DataType::Utf8, true),
46 Field::new("retention_time_sec", DataType::Float64, false),
47 Field::new("total_ion_current", DataType::Float64, true),
48 Field::new("base_peak_mz", DataType::Float64, true),
49 Field::new("base_peak_intensity", DataType::Float64, true),
50 Field::new("low_mz", DataType::Float64, true),
51 Field::new("high_mz", DataType::Float64, true),
52 Field::new("ion_injection_time_ms", DataType::Float64, true),
53 Field::new("inv_mobility", DataType::Float64, true),
54 Field::new("faims_cv", DataType::Float64, true),
55 Field::new("precursor_target_mz", DataType::Float64, true),
56 Field::new("precursor_selected_mz", DataType::Float64, true),
57 Field::new("precursor_isolation_width", DataType::Float64, true),
58 Field::new("precursor_charge", DataType::Int32, true),
59 Field::new("precursor_intensity", DataType::Float64, true),
60 Field::new("precursor_collision_energy", DataType::Float64, true),
61 Field::new("precursor_ce_is_nce", DataType::UInt8, true),
62 Field::new("precursor_native_id", DataType::Utf8, true),
63 Field::new("precursor_activation", DataType::Utf8, true),
64 Field::new("precursor_analyzer", DataType::Utf8, true),
65 Field::new("precursor_ccs", DataType::Float64, true),
66 Field::new_large_list("mz", mz_item, false),
67 Field::new_large_list("intensity", int_item, false),
68 Field::new_large_list("inv_mobility_per_peak", mob_item, true),
69 Field::new("mobility_array_kind", DataType::Utf8, true),
70 ]))
71}
72
73fn polarity_str(p: Polarity) -> &'static str {
74 match p {
75 Polarity::Positive => "positive",
76 Polarity::Negative => "negative",
77 }
78}
79
80fn scan_mode_str(m: ScanMode) -> &'static str {
81 match m {
82 ScanMode::Profile => "profile",
83 ScanMode::Centroid => "centroid",
84 }
85}
86
87fn analyzer_str(a: Analyzer) -> &'static str {
88 match a {
89 Analyzer::ITMS => "itms",
90 Analyzer::TQMS => "tqms",
91 Analyzer::SQMS => "sqms",
92 Analyzer::TOFMS => "tof",
93 Analyzer::FTMS => "ftms",
94 Analyzer::Sector => "sector",
95 }
96}
97
98fn activation_str(a: Activation) -> &'static str {
99 match a {
100 Activation::CID => "cid",
101 Activation::HCD => "hcd",
102 Activation::ETD => "etd",
103 Activation::ECD => "ecd",
104 Activation::UVPD => "uvpd",
105 Activation::PQD => "pqd",
106 Activation::PD => "pd",
107 Activation::SID => "sid",
108 Activation::EThcD => "ethcd",
109 Activation::IRMPD => "irmpd",
110 Activation::MPID => "mpid",
111 }
112}
113
114fn mobility_kind_str(k: MobilityArrayKind) -> &'static str {
115 match k {
116 MobilityArrayKind::InverseReducedVsPerCm2 => "inverse_reduced_k0",
117 MobilityArrayKind::DriftTimeMilliseconds => "drift_time_ms",
118 }
119}
120
121pub struct SpectrumBatchBuilder {
128 schema: SchemaRef,
129 mobility_kind: Option<MobilityArrayKind>,
130 index: UInt32Builder,
131 scan_number: UInt32Builder,
132 native_id: StringBuilder,
133 ms_level: UInt8Builder,
134 polarity: StringBuilder,
135 scan_mode: StringBuilder,
136 analyzer: StringBuilder,
137 filter: StringBuilder,
138 retention_time_sec: Float64Builder,
139 total_ion_current: Float64Builder,
140 base_peak_mz: Float64Builder,
141 base_peak_intensity: Float64Builder,
142 low_mz: Float64Builder,
143 high_mz: Float64Builder,
144 ion_injection_time_ms: Float64Builder,
145 inv_mobility: Float64Builder,
146 faims_cv: Float64Builder,
147 precursor_target_mz: Float64Builder,
148 precursor_selected_mz: Float64Builder,
149 precursor_isolation_width: Float64Builder,
150 precursor_charge: Int32Builder,
151 precursor_intensity: Float64Builder,
152 precursor_collision_energy: Float64Builder,
153 precursor_ce_is_nce: UInt8Builder,
154 precursor_native_id: StringBuilder,
155 precursor_activation: StringBuilder,
156 precursor_analyzer: StringBuilder,
157 precursor_ccs: Float64Builder,
158 mz: LargeListBuilder<Float64Builder>,
159 intensity: LargeListBuilder<Float32Builder>,
160 inv_mobility_per_peak: LargeListBuilder<Float32Builder>,
161 mobility_array_kind_col: StringBuilder,
162}
163
164impl SpectrumBatchBuilder {
165 pub fn new(mobility_kind: Option<MobilityArrayKind>) -> Self {
169 Self {
170 schema: spectrum_record_schema(),
171 mobility_kind,
172 index: UInt32Builder::new(),
173 scan_number: UInt32Builder::new(),
174 native_id: StringBuilder::new(),
175 ms_level: UInt8Builder::new(),
176 polarity: StringBuilder::new(),
177 scan_mode: StringBuilder::new(),
178 analyzer: StringBuilder::new(),
179 filter: StringBuilder::new(),
180 retention_time_sec: Float64Builder::new(),
181 total_ion_current: Float64Builder::new(),
182 base_peak_mz: Float64Builder::new(),
183 base_peak_intensity: Float64Builder::new(),
184 low_mz: Float64Builder::new(),
185 high_mz: Float64Builder::new(),
186 ion_injection_time_ms: Float64Builder::new(),
187 inv_mobility: Float64Builder::new(),
188 faims_cv: Float64Builder::new(),
189 precursor_target_mz: Float64Builder::new(),
190 precursor_selected_mz: Float64Builder::new(),
191 precursor_isolation_width: Float64Builder::new(),
192 precursor_charge: Int32Builder::new(),
193 precursor_intensity: Float64Builder::new(),
194 precursor_collision_energy: Float64Builder::new(),
195 precursor_ce_is_nce: UInt8Builder::new(),
196 precursor_native_id: StringBuilder::new(),
197 precursor_activation: StringBuilder::new(),
198 precursor_analyzer: StringBuilder::new(),
199 precursor_ccs: Float64Builder::new(),
200 mz: LargeListBuilder::new(Float64Builder::new()).with_field(Arc::new(Field::new(
201 "item",
202 DataType::Float64,
203 false,
204 ))),
205 intensity: LargeListBuilder::new(Float32Builder::new())
206 .with_field(Arc::new(Field::new("item", DataType::Float32, false))),
207 inv_mobility_per_peak: LargeListBuilder::new(Float32Builder::new())
208 .with_field(Arc::new(Field::new("item", DataType::Float32, false))),
209 mobility_array_kind_col: StringBuilder::new(),
210 }
211 }
212
213 pub fn schema(&self) -> SchemaRef {
215 self.schema.clone()
216 }
217
218 pub fn push(&mut self, rec: &SpectrumRecord) {
220 self.index.append_value(rec.index as u32);
221 self.scan_number.append_value(rec.scan_number);
222 self.native_id.append_value(&rec.native_id);
223 self.ms_level.append_value(rec.ms_level as u8);
224 self.polarity.append_option(rec.polarity.map(polarity_str));
225 self.scan_mode
226 .append_option(rec.scan_mode.map(scan_mode_str));
227 self.analyzer.append_option(rec.analyzer.map(analyzer_str));
228 self.filter.append_option(rec.filter.as_deref());
229 self.retention_time_sec.append_value(rec.retention_time_sec);
230 self.total_ion_current.append_option(rec.total_ion_current);
231 self.base_peak_mz.append_option(rec.base_peak_mz);
232 self.base_peak_intensity
233 .append_option(rec.base_peak_intensity);
234 self.low_mz.append_option(rec.low_mz);
235 self.high_mz.append_option(rec.high_mz);
236 self.ion_injection_time_ms
237 .append_option(rec.ion_injection_time_ms);
238 self.inv_mobility.append_option(rec.inv_mobility);
239 self.faims_cv.append_option(rec.faims_cv);
240
241 match &rec.precursor {
242 Some(p) => {
243 self.precursor_target_mz.append_option(p.target_mz);
244 self.precursor_selected_mz.append_option(p.selected_mz);
245 self.precursor_isolation_width
246 .append_option(p.isolation_width);
247 self.precursor_charge.append_option(p.charge);
248 self.precursor_intensity.append_option(p.intensity);
249 self.precursor_collision_energy
250 .append_option(p.collision_energy);
251 self.precursor_ce_is_nce.append_value(u8::from(p.ce_is_nce));
252 self.precursor_native_id
253 .append_option(p.precursor_native_id.as_deref());
254 self.precursor_activation
255 .append_option(p.activation.map(activation_str));
256 self.precursor_analyzer
257 .append_option(p.analyzer.map(analyzer_str));
258 self.precursor_ccs.append_option(p.ccs);
259 }
260 None => {
261 self.precursor_target_mz.append_null();
262 self.precursor_selected_mz.append_null();
263 self.precursor_isolation_width.append_null();
264 self.precursor_charge.append_null();
265 self.precursor_intensity.append_null();
266 self.precursor_collision_energy.append_null();
267 self.precursor_ce_is_nce.append_null();
268 self.precursor_native_id.append_null();
269 self.precursor_activation.append_null();
270 self.precursor_analyzer.append_null();
271 self.precursor_ccs.append_null();
272 }
273 }
274
275 for &v in &rec.mz {
276 self.mz.values().append_value(v);
277 }
278 self.mz.append(true);
279 for &v in &rec.intensity {
280 self.intensity.values().append_value(v);
281 }
282 self.intensity.append(true);
283 match &rec.inv_mobility_per_peak {
284 Some(mob) => {
285 for &v in mob {
286 self.inv_mobility_per_peak.values().append_value(v);
287 }
288 self.inv_mobility_per_peak.append(true);
289 }
290 None => self.inv_mobility_per_peak.append(false),
291 }
292 self.mobility_array_kind_col
293 .append_option(self.mobility_kind.map(mobility_kind_str));
294 }
295
296 pub fn len(&self) -> usize {
298 self.index.len()
299 }
300
301 pub fn is_empty(&self) -> bool {
303 self.len() == 0
304 }
305
306 pub fn finish(mut self) -> Result<RecordBatch, arrow_schema::ArrowError> {
308 let arrays: Vec<ArrayRef> = vec![
309 Arc::new(self.index.finish()),
310 Arc::new(self.scan_number.finish()),
311 Arc::new(self.native_id.finish()),
312 Arc::new(self.ms_level.finish()),
313 Arc::new(self.polarity.finish()),
314 Arc::new(self.scan_mode.finish()),
315 Arc::new(self.analyzer.finish()),
316 Arc::new(self.filter.finish()),
317 Arc::new(self.retention_time_sec.finish()),
318 Arc::new(self.total_ion_current.finish()),
319 Arc::new(self.base_peak_mz.finish()),
320 Arc::new(self.base_peak_intensity.finish()),
321 Arc::new(self.low_mz.finish()),
322 Arc::new(self.high_mz.finish()),
323 Arc::new(self.ion_injection_time_ms.finish()),
324 Arc::new(self.inv_mobility.finish()),
325 Arc::new(self.faims_cv.finish()),
326 Arc::new(self.precursor_target_mz.finish()),
327 Arc::new(self.precursor_selected_mz.finish()),
328 Arc::new(self.precursor_isolation_width.finish()),
329 Arc::new(self.precursor_charge.finish()),
330 Arc::new(self.precursor_intensity.finish()),
331 Arc::new(self.precursor_collision_energy.finish()),
332 Arc::new(self.precursor_ce_is_nce.finish()),
333 Arc::new(self.precursor_native_id.finish()),
334 Arc::new(self.precursor_activation.finish()),
335 Arc::new(self.precursor_analyzer.finish()),
336 Arc::new(self.precursor_ccs.finish()),
337 Arc::new(self.mz.finish()),
338 Arc::new(self.intensity.finish()),
339 Arc::new(self.inv_mobility_per_peak.finish()),
340 Arc::new(self.mobility_array_kind_col.finish()),
341 ];
342 RecordBatch::try_new(self.schema.clone(), arrays)
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::{PrecursorInfo, SpectrumRecord};
350 use arrow_array::Array;
351
352 fn rec(index: usize, ms_level: u32, n_peaks: usize, with_mob: bool) -> SpectrumRecord {
353 let mz: Vec<f64> = (0..n_peaks).map(|i| 100.0 + i as f64).collect();
354 let intensity: Vec<f32> = (0..n_peaks).map(|i| 10.0 + i as f32).collect();
355 let mobility = if with_mob {
356 Some((0..n_peaks).map(|i| 0.5 + i as f32 * 0.01).collect())
357 } else {
358 None
359 };
360 SpectrumRecord {
361 index,
362 scan_number: (index + 1) as u32,
363 native_id: format!("scan={}", index + 1),
364 ms_level,
365 polarity: Some(Polarity::Positive),
366 scan_mode: Some(ScanMode::Centroid),
367 analyzer: Some(Analyzer::TOFMS),
368 filter: None,
369 retention_time_sec: index as f64,
370 total_ion_current: Some(intensity.iter().map(|&v| v as f64).sum()),
371 base_peak_mz: mz.last().copied(),
372 base_peak_intensity: intensity.last().map(|&v| v as f64),
373 low_mz: mz.first().copied(),
374 high_mz: mz.last().copied(),
375 ion_injection_time_ms: None,
376 inv_mobility: None,
377 faims_cv: if index == 0 { Some(-40.0) } else { None },
378 precursor: if ms_level >= 2 {
379 Some(PrecursorInfo {
380 target_mz: Some(500.0),
381 selected_mz: Some(500.5),
382 isolation_width: Some(2.0),
383 charge: Some(2),
384 ..Default::default()
385 })
386 } else {
387 None
388 },
389 mz,
390 intensity,
391 inv_mobility_per_peak: mobility,
392 }
393 }
394
395 #[test]
396 fn schema_round_trip() {
397 let mut b = SpectrumBatchBuilder::new(Some(MobilityArrayKind::DriftTimeMilliseconds));
398 b.push(&rec(0, 1, 3, true));
399 b.push(&rec(1, 2, 4, false));
400 let batch = b.finish().unwrap();
401 assert_eq!(batch.num_rows(), 2);
402 assert_eq!(batch.schema().fields().len(), 32);
403 let mz_col = batch
404 .column_by_name("mz")
405 .unwrap()
406 .as_any()
407 .downcast_ref::<arrow_array::LargeListArray>()
408 .unwrap();
409 assert_eq!(mz_col.value_length(0), 3);
410 assert_eq!(mz_col.value_length(1), 4);
411 let mob_col = batch
412 .column_by_name("inv_mobility_per_peak")
413 .unwrap()
414 .as_any()
415 .downcast_ref::<arrow_array::LargeListArray>()
416 .unwrap();
417 assert!(mob_col.is_valid(0));
418 assert!(mob_col.is_null(1));
419 let faims_col = batch
420 .column_by_name("faims_cv")
421 .unwrap()
422 .as_any()
423 .downcast_ref::<arrow_array::Float64Array>()
424 .unwrap();
425 assert_eq!(faims_col.value(0), -40.0);
426 assert!(faims_col.is_null(1));
427 }
428}