use std::path::Path;
use byteorder::{ByteOrder, LittleEndian};
use cfb::CompoundFile;
use openmassspec_core::{
Analyzer, CvTerm, PrecursorInfo, RunMetadata, ScanMode, SpectrumRecord, SpectrumSource,
};
use crate::raw::{self, qgd, qtfl, ttfl, Variant};
const GCMS_SPECTRUM_INDEX: &str = "GCMS Raw Data/Spectrum Index";
const GCMS_MS_RAW_DATA: &str = "GCMS Raw Data/MS Raw Data";
const QTFL_CENTROID_INDEX: &str = "QTFL RawData/Centroid Index";
const QTFL_CENTROID_DATA: &str = "QTFL RawData/Centroid Data";
const QTFL_RETENTION_TIME: &str = "QTFL RawData/Retention Time";
const TTFL_DATA_INDEX: &str = "TTFL Raw Data/Data Index";
const TTFL_MS_RAW_DATA: &str = "TTFL Raw Data/MS Raw Data";
const TTFL_RETENTION_TIME: &str = "TTFL Raw Data/Retention Time";
const TTFL_TUNING_RESULT: [&str; 3] = [
"TTFL Tuning/Tuning Result 00",
"TTFL Tuning/Tuning Result 01",
"TTFL Tuning/Tuning Result 02",
];
enum Decoded {
Qgd {
ms_raw: Vec<u8>,
offsets: Vec<u64>,
},
Qtfl {
centroid_data: Vec<u8>,
records: Vec<qtfl::CentroidIndexRecord>,
retention_time_ms: Vec<u32>,
},
Ttfl {
ms_raw: Vec<u8>,
subsets: Vec<ttfl::DataIndexSubset>,
bounds: Vec<(u32, u32)>,
retention_time_ms: Vec<u32>,
calibration: Option<ttfl::Calibration>,
},
}
pub struct Reader {
pub stem: String,
variant: Variant,
decoded: Decoded,
start_timestamp: Option<String>,
}
fn parse_u32_array(data: &[u8]) -> Vec<u32> {
let n = data.len() / 4;
(0..n)
.map(|i| LittleEndian::read_u32(&data[i * 4..i * 4 + 4]))
.collect()
}
impl Reader {
pub fn open<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
let path = path.as_ref();
let ext_lower = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let file = std::fs::File::open(path)?;
let mut comp = CompoundFile::open(file)
.map_err(|e| crate::Error::Parse(format!("not a valid OLE2/CFBF container: {e}")))?;
let variant = raw::detect_variant(&ext_lower, &mut comp)?;
let start_timestamp = raw::timestamp::earliest_created_timestamp(&mut comp);
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
let decoded = match variant {
Variant::Qgd => {
let si = raw::read_stream(&mut comp, GCMS_SPECTRUM_INDEX)?;
let ms_raw = raw::read_stream(&mut comp, GCMS_MS_RAW_DATA)?;
let offsets = qgd::parse_spectrum_index(&si)?;
Decoded::Qgd { ms_raw, offsets }
}
Variant::Qtfl => {
let ci = raw::read_stream(&mut comp, QTFL_CENTROID_INDEX)?;
let centroid_data = raw::read_stream(&mut comp, QTFL_CENTROID_DATA)?;
let rt = raw::read_stream(&mut comp, QTFL_RETENTION_TIME)?;
let records = qtfl::parse_centroid_index(&ci)?;
let retention_time_ms = qtfl::parse_retention_time(&rt)?;
Decoded::Qtfl {
centroid_data,
records,
retention_time_ms,
}
}
Variant::Ttfl => {
let di = raw::read_stream(&mut comp, TTFL_DATA_INDEX)?;
let ms_raw = raw::read_stream(&mut comp, TTFL_MS_RAW_DATA)?;
let rt = raw::read_stream(&mut comp, TTFL_RETENTION_TIME)?;
let subsets = ttfl::parse_data_index(&di)?;
let bounds = ttfl::scan_bounds(&subsets, ms_raw.len());
let retention_time_ms = parse_u32_array(&rt);
let calibration = TTFL_TUNING_RESULT
.iter()
.find_map(|path| raw::read_stream_opt(&mut comp, path))
.and_then(|data| ttfl::parse_calibration(&data));
Decoded::Ttfl {
ms_raw,
subsets,
bounds,
retention_time_ms,
calibration,
}
}
};
Ok(Reader {
stem,
variant,
decoded,
start_timestamp,
})
}
}
fn qgd_spectra(stem: &str, ms_raw: &[u8], offsets: &[u64]) -> Vec<SpectrumRecord> {
let n = offsets.len();
let mut out = Vec::new();
for i in 0..n {
let start = offsets[i] as usize;
let end = if i + 1 < n {
offsets[i + 1] as usize
} else {
ms_raw.len()
};
if start > end || end > ms_raw.len() {
continue; }
let scan = match qgd::parse_scan(&ms_raw[start..end]) {
Ok(s) => s,
Err(_) => continue,
};
let rt_sec = scan.retention_time_ms() as f64 / 1000.0;
match scan {
qgd::QgdScan::Profile { mz, intensity, .. } => {
let idx = out.len();
out.push(SpectrumRecord {
index: idx,
scan_number: (idx + 1) as u32,
native_id: format!("source={stem} start={} end={}", idx + 1, idx + 1),
ms_level: 1,
polarity: None,
scan_mode: Some(ScanMode::Profile),
analyzer: Some(Analyzer::SQMS),
filter: None,
retention_time_sec: rt_sec,
total_ion_current: None,
base_peak_mz: None,
base_peak_intensity: None,
low_mz: None,
high_mz: None,
ion_injection_time_ms: None,
inv_mobility: None,
faims_cv: None, precursor: None,
mz,
intensity,
inv_mobility_per_peak: None,
});
}
qgd::QgdScan::Mrm { transitions, .. } => {
for t in transitions {
let idx = out.len();
out.push(SpectrumRecord {
index: idx,
scan_number: (idx + 1) as u32,
native_id: format!("source={stem} start={} end={}", idx + 1, idx + 1),
ms_level: 2,
polarity: None,
scan_mode: Some(ScanMode::Centroid),
analyzer: Some(Analyzer::TQMS),
filter: None,
retention_time_sec: rt_sec,
total_ion_current: None,
base_peak_mz: None,
base_peak_intensity: None,
low_mz: None,
high_mz: None,
ion_injection_time_ms: None,
inv_mobility: None,
faims_cv: None,
precursor: Some(PrecursorInfo {
target_mz: Some(t.precursor_mz),
selected_mz: Some(t.precursor_mz),
..Default::default()
}),
mz: vec![t.product_mz],
intensity: vec![t.intensity],
inv_mobility_per_peak: None,
});
}
}
}
}
out
}
fn qtfl_spectra(
centroid_data: &[u8],
records: &[qtfl::CentroidIndexRecord],
retention_time_ms: &[u32],
) -> Vec<SpectrumRecord> {
let n = records.len();
let mut out = Vec::with_capacity(n);
let mut last_ms1_native_id: Option<String> = None;
for i in 0..n {
let start = records[i].offset as usize;
let end = if i + 1 < n {
records[i + 1].offset as usize
} else {
centroid_data.len()
};
if start > end || end > centroid_data.len() {
continue;
}
let spec = match qtfl::decode_scan(¢roid_data[start..end]) {
Ok(s) => s,
Err(_) => continue,
};
let rt_ms = retention_time_ms.get(i).copied().unwrap_or(0);
let is_ms1 = records[i].event_id <= 1;
let idx = out.len();
let native_id = format!("scan={}", idx + 1);
if is_ms1 {
last_ms1_native_id = Some(native_id.clone());
}
out.push(SpectrumRecord {
index: idx,
scan_number: (idx + 1) as u32,
native_id,
ms_level: if is_ms1 { 1 } else { 2 },
polarity: None,
scan_mode: Some(ScanMode::Centroid),
analyzer: Some(Analyzer::TOFMS),
filter: None,
retention_time_sec: rt_ms as f64 / 1000.0,
total_ion_current: None,
base_peak_mz: None,
base_peak_intensity: spec.base_peak_intensity,
low_mz: None,
high_mz: None,
ion_injection_time_ms: None,
inv_mobility: None,
faims_cv: None, precursor: if is_ms1 {
None
} else {
Some(PrecursorInfo {
precursor_native_id: last_ms1_native_id.clone(),
..Default::default()
})
},
mz: spec.mz,
intensity: spec.intensity,
inv_mobility_per_peak: None,
});
}
out
}
fn ttfl_spectra(
stem: &str,
ms_raw: &[u8],
subsets: &[ttfl::DataIndexSubset],
bounds: &[(u32, u32)],
retention_time_ms: &[u32],
calibration: Option<&ttfl::Calibration>,
) -> Vec<SpectrumRecord> {
let mut out = Vec::with_capacity(subsets.len());
for (subset, &(start, end)) in subsets.iter().zip(bounds.iter()) {
let start = start as usize;
let end = end as usize;
if start > end || end > ms_raw.len() {
continue;
}
let Some(spec) = ttfl::decode_scan(&ms_raw[start..end]) else {
continue;
};
let rt_ms = retention_time_ms.get(subset.entry_i).copied().unwrap_or(0);
let idx = out.len();
let mz = match calibration {
Some(cal) => spec.index_axis.iter().map(|&i| cal.mz(i)).collect(),
None => spec.index_axis,
};
out.push(SpectrumRecord {
index: idx,
scan_number: (idx + 1) as u32,
native_id: format!("source={stem} start={} end={}", idx + 1, idx + 1),
ms_level: 1,
polarity: None,
scan_mode: Some(ScanMode::Profile),
analyzer: Some(Analyzer::TOFMS),
filter: None,
retention_time_sec: rt_ms as f64 / 1000.0,
total_ion_current: None,
base_peak_mz: None,
base_peak_intensity: None,
low_mz: None,
high_mz: None,
ion_injection_time_ms: None,
inv_mobility: None,
faims_cv: None, precursor: None,
mz,
intensity: spec.intensity,
inv_mobility_per_peak: None,
});
}
out
}
impl SpectrumSource for Reader {
fn run_metadata(&self) -> RunMetadata {
match self.variant {
Variant::Qgd => RunMetadata {
source_file_name: format!("{}.qgd", self.stem),
source_file_format: CvTerm::new("MS:1000560", "Shimadzu GCMSsolution QGD format"),
native_id_format: CvTerm::new("MS:1000929", "Shimadzu Biotech nativeID format"),
instrument: CvTerm::new("MS:1000124", "Shimadzu instrument model"),
software_name: "openszraw".to_string(),
software_version: env!("CARGO_PKG_VERSION").to_string(),
start_timestamp: self.start_timestamp.clone(),
mobility_array_kind: None,
},
Variant::Qtfl => RunMetadata {
source_file_name: format!("{}.lcd", self.stem),
source_file_format: CvTerm::new("MS:1003009", "Shimadzu Biotech LCD format"),
native_id_format: CvTerm::new(
"MS:1002898",
"Shimadzu Biotech QTOF nativeID format",
),
instrument: CvTerm::new("MS:1002998", "LCMS-9030"),
software_name: "openszraw".to_string(),
software_version: env!("CARGO_PKG_VERSION").to_string(),
start_timestamp: self.start_timestamp.clone(),
mobility_array_kind: None,
},
Variant::Ttfl => RunMetadata {
source_file_name: format!("{}.lcd", self.stem),
source_file_format: CvTerm::new("MS:1003009", "Shimadzu Biotech LCD format"),
native_id_format: CvTerm::new("MS:1000929", "Shimadzu Biotech nativeID format"),
instrument: CvTerm::new("MS:1000604", "LCMS-IT-TOF"),
software_name: "openszraw".to_string(),
software_version: env!("CARGO_PKG_VERSION").to_string(),
start_timestamp: self.start_timestamp.clone(),
mobility_array_kind: None,
},
}
}
fn spectrum_count_hint(&self) -> Option<usize> {
match &self.decoded {
Decoded::Qgd { offsets, .. } => Some(offsets.len()),
Decoded::Qtfl { records, .. } => Some(records.len()),
Decoded::Ttfl { subsets, .. } => Some(subsets.len()),
}
}
fn iter_spectra<'a>(&'a mut self) -> Box<dyn Iterator<Item = SpectrumRecord> + 'a> {
let spectra = match &self.decoded {
Decoded::Qgd { ms_raw, offsets } => qgd_spectra(&self.stem, ms_raw, offsets),
Decoded::Qtfl {
centroid_data,
records,
retention_time_ms,
} => qtfl_spectra(centroid_data, records, retention_time_ms),
Decoded::Ttfl {
ms_raw,
subsets,
bounds,
retention_time_ms,
calibration,
} => ttfl_spectra(
&self.stem,
ms_raw,
subsets,
bounds,
retention_time_ms,
calibration.as_ref(),
),
};
Box::new(spectra.into_iter())
}
}