1use std::sync::Arc;
36
37use arrow_array::builder::{
38 ArrayBuilder, Float32Builder, Float64Builder, Int32Builder, LargeListBuilder, MapBuilder,
39 MapFieldNames, StringBuilder, UInt32Builder, UInt8Builder,
40};
41use arrow_array::{ArrayRef, RecordBatch};
42use arrow_schema::{DataType, Field, Schema, SchemaRef};
43
44use openmassspec_core::{
45 Activation, Analyzer, MobilityArrayKind, Polarity, ScanMode, SpectrumRecord,
46};
47
48const EXTRA_ENTRIES_FIELD: &str = "entries";
50const EXTRA_KEY_FIELD: &str = "key";
51const EXTRA_VALUE_FIELD: &str = "value";
52
53pub fn spectrum_record_schema() -> SchemaRef {
55 let mz_item = Arc::new(Field::new("item", DataType::Float64, false));
56 let int_item = Arc::new(Field::new("item", DataType::Float32, false));
57 let mob_item = Arc::new(Field::new("item", DataType::Float32, false));
58 Arc::new(Schema::new(vec![
59 Field::new("index", DataType::UInt32, false),
60 Field::new("scan_number", DataType::UInt32, false),
61 Field::new("native_id", DataType::Utf8, false),
62 Field::new("ms_level", DataType::UInt8, false),
63 Field::new("polarity", DataType::Utf8, true),
64 Field::new("scan_mode", DataType::Utf8, true),
65 Field::new("analyzer", DataType::Utf8, true),
66 Field::new("filter", DataType::Utf8, true),
67 Field::new("retention_time_sec", DataType::Float64, false),
68 Field::new("total_ion_current", DataType::Float64, true),
69 Field::new("base_peak_mz", DataType::Float64, true),
70 Field::new("base_peak_intensity", DataType::Float64, true),
71 Field::new("low_mz", DataType::Float64, true),
72 Field::new("high_mz", DataType::Float64, true),
73 Field::new("ion_injection_time_ms", DataType::Float64, true),
74 Field::new("inv_mobility", DataType::Float64, true),
75 Field::new("faims_cv", DataType::Float64, true),
76 Field::new("precursor_target_mz", DataType::Float64, true),
77 Field::new("precursor_selected_mz", DataType::Float64, true),
78 Field::new("precursor_isolation_width", DataType::Float64, true),
79 Field::new("precursor_charge", DataType::Int32, true),
80 Field::new("precursor_intensity", DataType::Float64, true),
81 Field::new("precursor_collision_energy", DataType::Float64, true),
82 Field::new("precursor_ce_is_nce", DataType::UInt8, true),
83 Field::new("precursor_native_id", DataType::Utf8, true),
84 Field::new("precursor_activation", DataType::Utf8, true),
85 Field::new("precursor_analyzer", DataType::Utf8, true),
86 Field::new("precursor_ccs", DataType::Float64, true),
87 Field::new_large_list("mz", mz_item, false),
88 Field::new_large_list("intensity", int_item, false),
89 Field::new_large_list("inv_mobility_per_peak", mob_item, true),
90 Field::new("mobility_array_kind", DataType::Utf8, true),
91 Field::new("acquisition_event_id", DataType::UInt32, true),
92 Field::new_map(
93 "extra",
94 EXTRA_ENTRIES_FIELD,
95 Field::new(EXTRA_KEY_FIELD, DataType::Utf8, false),
96 Field::new(EXTRA_VALUE_FIELD, DataType::Utf8, true),
97 false,
98 false,
99 ),
100 ]))
101}
102
103pub struct SpectrumBatchBuilder {
110 schema: SchemaRef,
111 mobility_kind: Option<MobilityArrayKind>,
112 index: UInt32Builder,
113 scan_number: UInt32Builder,
114 native_id: StringBuilder,
115 ms_level: UInt8Builder,
116 polarity: StringBuilder,
117 scan_mode: StringBuilder,
118 analyzer: StringBuilder,
119 filter: StringBuilder,
120 retention_time_sec: Float64Builder,
121 total_ion_current: Float64Builder,
122 base_peak_mz: Float64Builder,
123 base_peak_intensity: Float64Builder,
124 low_mz: Float64Builder,
125 high_mz: Float64Builder,
126 ion_injection_time_ms: Float64Builder,
127 inv_mobility: Float64Builder,
128 faims_cv: Float64Builder,
129 precursor_target_mz: Float64Builder,
130 precursor_selected_mz: Float64Builder,
131 precursor_isolation_width: Float64Builder,
132 precursor_charge: Int32Builder,
133 precursor_intensity: Float64Builder,
134 precursor_collision_energy: Float64Builder,
135 precursor_ce_is_nce: UInt8Builder,
136 precursor_native_id: StringBuilder,
137 precursor_activation: StringBuilder,
138 precursor_analyzer: StringBuilder,
139 precursor_ccs: Float64Builder,
140 mz: LargeListBuilder<Float64Builder>,
141 intensity: LargeListBuilder<Float32Builder>,
142 inv_mobility_per_peak: LargeListBuilder<Float32Builder>,
143 mobility_array_kind_col: StringBuilder,
144 acquisition_event_id: UInt32Builder,
145 extra: MapBuilder<StringBuilder, StringBuilder>,
146}
147
148impl SpectrumBatchBuilder {
149 pub fn new(mobility_kind: Option<MobilityArrayKind>) -> Self {
153 Self {
154 schema: spectrum_record_schema(),
155 mobility_kind,
156 index: UInt32Builder::new(),
157 scan_number: UInt32Builder::new(),
158 native_id: StringBuilder::new(),
159 ms_level: UInt8Builder::new(),
160 polarity: StringBuilder::new(),
161 scan_mode: StringBuilder::new(),
162 analyzer: StringBuilder::new(),
163 filter: StringBuilder::new(),
164 retention_time_sec: Float64Builder::new(),
165 total_ion_current: Float64Builder::new(),
166 base_peak_mz: Float64Builder::new(),
167 base_peak_intensity: Float64Builder::new(),
168 low_mz: Float64Builder::new(),
169 high_mz: Float64Builder::new(),
170 ion_injection_time_ms: Float64Builder::new(),
171 inv_mobility: Float64Builder::new(),
172 faims_cv: Float64Builder::new(),
173 precursor_target_mz: Float64Builder::new(),
174 precursor_selected_mz: Float64Builder::new(),
175 precursor_isolation_width: Float64Builder::new(),
176 precursor_charge: Int32Builder::new(),
177 precursor_intensity: Float64Builder::new(),
178 precursor_collision_energy: Float64Builder::new(),
179 precursor_ce_is_nce: UInt8Builder::new(),
180 precursor_native_id: StringBuilder::new(),
181 precursor_activation: StringBuilder::new(),
182 precursor_analyzer: StringBuilder::new(),
183 precursor_ccs: Float64Builder::new(),
184 mz: LargeListBuilder::new(Float64Builder::new()).with_field(Arc::new(Field::new(
185 "item",
186 DataType::Float64,
187 false,
188 ))),
189 intensity: LargeListBuilder::new(Float32Builder::new())
190 .with_field(Arc::new(Field::new("item", DataType::Float32, false))),
191 inv_mobility_per_peak: LargeListBuilder::new(Float32Builder::new())
192 .with_field(Arc::new(Field::new("item", DataType::Float32, false))),
193 mobility_array_kind_col: StringBuilder::new(),
194 acquisition_event_id: UInt32Builder::new(),
195 extra: MapBuilder::new(
198 Some(MapFieldNames {
199 entry: EXTRA_ENTRIES_FIELD.to_string(),
200 key: EXTRA_KEY_FIELD.to_string(),
201 value: EXTRA_VALUE_FIELD.to_string(),
202 }),
203 StringBuilder::new(),
204 StringBuilder::new(),
205 ),
206 }
207 }
208
209 pub fn schema(&self) -> SchemaRef {
211 self.schema.clone()
212 }
213
214 pub fn push(&mut self, rec: &SpectrumRecord) {
216 self.index.append_value(rec.index as u32);
217 self.scan_number.append_value(rec.scan_number);
218 self.native_id.append_value(&rec.native_id);
219 self.ms_level.append_value(rec.ms_level as u8);
220 self.polarity
221 .append_option(rec.polarity.map(Polarity::as_str));
222 self.scan_mode
223 .append_option(rec.scan_mode.map(ScanMode::as_str));
224 self.analyzer
225 .append_option(rec.analyzer.map(Analyzer::as_str));
226 self.filter.append_option(rec.filter.as_deref());
227 self.retention_time_sec.append_value(rec.retention_time_sec);
228 self.total_ion_current.append_option(rec.total_ion_current);
229 self.base_peak_mz.append_option(rec.base_peak_mz);
230 self.base_peak_intensity
231 .append_option(rec.base_peak_intensity);
232 self.low_mz.append_option(rec.low_mz);
233 self.high_mz.append_option(rec.high_mz);
234 self.ion_injection_time_ms
235 .append_option(rec.ion_injection_time_ms);
236 self.inv_mobility.append_option(rec.inv_mobility);
237 self.faims_cv.append_option(rec.faims_cv);
238
239 match &rec.precursor {
240 Some(p) => {
241 self.precursor_target_mz.append_option(p.target_mz);
242 self.precursor_selected_mz.append_option(p.selected_mz);
243 self.precursor_isolation_width
244 .append_option(p.isolation_width);
245 self.precursor_charge.append_option(p.charge);
246 self.precursor_intensity.append_option(p.intensity);
247 self.precursor_collision_energy
248 .append_option(p.collision_energy);
249 self.precursor_ce_is_nce.append_value(u8::from(p.ce_is_nce));
250 self.precursor_native_id
251 .append_option(p.precursor_native_id.as_deref());
252 self.precursor_activation
253 .append_option(p.activation.map(Activation::as_str));
254 self.precursor_analyzer
255 .append_option(p.analyzer.map(Analyzer::as_str));
256 self.precursor_ccs.append_option(p.ccs);
257 }
258 None => {
259 self.precursor_target_mz.append_null();
260 self.precursor_selected_mz.append_null();
261 self.precursor_isolation_width.append_null();
262 self.precursor_charge.append_null();
263 self.precursor_intensity.append_null();
264 self.precursor_collision_energy.append_null();
265 self.precursor_ce_is_nce.append_null();
266 self.precursor_native_id.append_null();
267 self.precursor_activation.append_null();
268 self.precursor_analyzer.append_null();
269 self.precursor_ccs.append_null();
270 }
271 }
272
273 for &v in &rec.mz {
274 self.mz.values().append_value(v);
275 }
276 self.mz.append(true);
277 for &v in &rec.intensity {
278 self.intensity.values().append_value(v);
279 }
280 self.intensity.append(true);
281 match &rec.inv_mobility_per_peak {
282 Some(mob) => {
283 for &v in mob {
284 self.inv_mobility_per_peak.values().append_value(v);
285 }
286 self.inv_mobility_per_peak.append(true);
287 }
288 None => self.inv_mobility_per_peak.append(false),
289 }
290 self.mobility_array_kind_col
291 .append_option(self.mobility_kind.map(MobilityArrayKind::as_str));
292 self.acquisition_event_id
293 .append_option(rec.acquisition_event_id);
294 for (key, value) in &rec.extra {
295 self.extra.keys().append_value(key);
296 self.extra.values().append_value(value);
297 }
298 self.extra
299 .append(true)
300 .expect("extra keys and values are appended together");
301 }
302
303 pub fn len(&self) -> usize {
305 self.index.len()
306 }
307
308 pub fn is_empty(&self) -> bool {
310 self.len() == 0
311 }
312
313 pub fn finish(mut self) -> Result<RecordBatch, arrow_schema::ArrowError> {
315 let arrays: Vec<ArrayRef> = vec![
316 Arc::new(self.index.finish()),
317 Arc::new(self.scan_number.finish()),
318 Arc::new(self.native_id.finish()),
319 Arc::new(self.ms_level.finish()),
320 Arc::new(self.polarity.finish()),
321 Arc::new(self.scan_mode.finish()),
322 Arc::new(self.analyzer.finish()),
323 Arc::new(self.filter.finish()),
324 Arc::new(self.retention_time_sec.finish()),
325 Arc::new(self.total_ion_current.finish()),
326 Arc::new(self.base_peak_mz.finish()),
327 Arc::new(self.base_peak_intensity.finish()),
328 Arc::new(self.low_mz.finish()),
329 Arc::new(self.high_mz.finish()),
330 Arc::new(self.ion_injection_time_ms.finish()),
331 Arc::new(self.inv_mobility.finish()),
332 Arc::new(self.faims_cv.finish()),
333 Arc::new(self.precursor_target_mz.finish()),
334 Arc::new(self.precursor_selected_mz.finish()),
335 Arc::new(self.precursor_isolation_width.finish()),
336 Arc::new(self.precursor_charge.finish()),
337 Arc::new(self.precursor_intensity.finish()),
338 Arc::new(self.precursor_collision_energy.finish()),
339 Arc::new(self.precursor_ce_is_nce.finish()),
340 Arc::new(self.precursor_native_id.finish()),
341 Arc::new(self.precursor_activation.finish()),
342 Arc::new(self.precursor_analyzer.finish()),
343 Arc::new(self.precursor_ccs.finish()),
344 Arc::new(self.mz.finish()),
345 Arc::new(self.intensity.finish()),
346 Arc::new(self.inv_mobility_per_peak.finish()),
347 Arc::new(self.mobility_array_kind_col.finish()),
348 Arc::new(self.acquisition_event_id.finish()),
349 Arc::new(self.extra.finish()),
350 ];
351 RecordBatch::try_new(self.schema.clone(), arrays)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use arrow_array::Array;
359 use openmassspec_core::PrecursorInfo;
360
361 fn rec(index: usize, ms_level: u32, n_peaks: usize, with_mob: bool) -> SpectrumRecord {
362 let mz: Vec<f64> = (0..n_peaks).map(|i| 100.0 + i as f64).collect();
363 let intensity: Vec<f32> = (0..n_peaks).map(|i| 10.0 + i as f32).collect();
364 let mobility = if with_mob {
365 Some((0..n_peaks).map(|i| 0.5 + i as f32 * 0.01).collect())
366 } else {
367 None
368 };
369 let mut rec = SpectrumRecord::new(index, (index + 1) as u32, format!("scan={}", index + 1))
370 .with_ms_level(ms_level)
371 .with_polarity(Polarity::Positive)
372 .with_scan_mode(ScanMode::Centroid)
373 .with_analyzer(Analyzer::TOFMS)
374 .with_acquisition_event_id((index == 0).then_some(7))
375 .with_retention_time_sec(index as f64)
376 .with_total_ion_current(intensity.iter().map(|&v| v as f64).sum::<f64>())
377 .with_base_peak_mz(mz.last().copied())
378 .with_base_peak_intensity(intensity.last().map(|&v| v as f64))
379 .with_low_mz(mz.first().copied())
380 .with_high_mz(mz.last().copied())
381 .with_faims_cv(if index == 0 { Some(-40.0) } else { None })
382 .with_inv_mobility_per_peak(mobility)
383 .with_peaks(mz, intensity);
384 if ms_level >= 2 {
385 rec.precursor = Some(
386 PrecursorInfo::default()
387 .with_target_mz(500.0)
388 .with_selected_mz(500.5)
389 .with_isolation_width(2.0)
390 .with_charge(2),
391 );
392 }
393 if index == 0 {
394 rec = rec.with_extra("opentfraw.resolution", "120000");
395 }
396 rec
397 }
398
399 #[test]
400 fn schema_round_trip() {
401 let mut b = SpectrumBatchBuilder::new(Some(MobilityArrayKind::DriftTimeMilliseconds));
402 b.push(&rec(0, 1, 3, true));
403 b.push(&rec(1, 2, 4, false));
404 let batch = b.finish().unwrap();
405 assert_eq!(batch.num_rows(), 2);
406 assert_eq!(batch.schema().fields().len(), 34);
407 let mz_col = batch
408 .column_by_name("mz")
409 .unwrap()
410 .as_any()
411 .downcast_ref::<arrow_array::LargeListArray>()
412 .unwrap();
413 assert_eq!(mz_col.value_length(0), 3);
414 assert_eq!(mz_col.value_length(1), 4);
415 let mob_col = batch
416 .column_by_name("inv_mobility_per_peak")
417 .unwrap()
418 .as_any()
419 .downcast_ref::<arrow_array::LargeListArray>()
420 .unwrap();
421 assert!(mob_col.is_valid(0));
422 assert!(mob_col.is_null(1));
423 let faims_col = batch
424 .column_by_name("faims_cv")
425 .unwrap()
426 .as_any()
427 .downcast_ref::<arrow_array::Float64Array>()
428 .unwrap();
429 assert_eq!(faims_col.value(0), -40.0);
430 assert!(faims_col.is_null(1));
431 let event_col = batch
432 .column_by_name("acquisition_event_id")
433 .unwrap()
434 .as_any()
435 .downcast_ref::<arrow_array::UInt32Array>()
436 .unwrap();
437 assert_eq!(event_col.value(0), 7);
438 assert!(event_col.is_null(1));
439 let extra_col = batch
440 .column_by_name("extra")
441 .unwrap()
442 .as_any()
443 .downcast_ref::<arrow_array::MapArray>()
444 .unwrap();
445 assert_eq!(extra_col.value_length(0), 1);
446 assert_eq!(extra_col.value_length(1), 0);
447 let keys = extra_col
448 .keys()
449 .as_any()
450 .downcast_ref::<arrow_array::StringArray>()
451 .unwrap();
452 let values = extra_col
453 .values()
454 .as_any()
455 .downcast_ref::<arrow_array::StringArray>()
456 .unwrap();
457 assert_eq!(keys.value(0), "opentfraw.resolution");
458 assert_eq!(values.value(0), "120000");
459 }
460}