use byteorder::{ByteOrder, LittleEndian};
pub const INDEX_RECORD_SIZE: usize = 24;
pub const SCAN_HEADER_SIZE: usize = 64;
pub const RT_RECORD_SIZE: usize = 12;
#[derive(Debug, Clone, Copy)]
pub struct CentroidIndexRecord {
pub offset: u32,
pub cycle_index: u32,
pub event_id: u32,
}
pub fn parse_centroid_index(data: &[u8]) -> crate::Result<Vec<CentroidIndexRecord>> {
if data.len() % INDEX_RECORD_SIZE != 0 {
return Err(crate::Error::Parse(format!(
"Centroid Index stream size {} is not a multiple of {INDEX_RECORD_SIZE}",
data.len()
)));
}
let n = data.len() / INDEX_RECORD_SIZE;
let mut records = Vec::with_capacity(n);
for i in 0..n {
let rec = &data[i * INDEX_RECORD_SIZE..(i + 1) * INDEX_RECORD_SIZE];
let offset = LittleEndian::read_u32(&rec[0..4]);
let cycle_index = LittleEndian::read_u32(&rec[8..12]);
let event_id = LittleEndian::read_u32(&rec[20..24]);
records.push(CentroidIndexRecord {
offset,
cycle_index,
event_id,
});
}
Ok(records)
}
pub fn parse_retention_time(data: &[u8]) -> crate::Result<Vec<u32>> {
if data.len() % RT_RECORD_SIZE != 0 {
return Err(crate::Error::Parse(format!(
"Retention Time stream size {} is not a multiple of {RT_RECORD_SIZE}",
data.len()
)));
}
let n = data.len() / RT_RECORD_SIZE;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let rec = &data[i * RT_RECORD_SIZE..(i + 1) * RT_RECORD_SIZE];
out.push(LittleEndian::read_u32(&rec[0..4]));
}
Ok(out)
}
#[derive(Debug, Clone, Default)]
pub struct QtflSpectrum {
pub mz: Vec<f64>,
pub intensity: Vec<f32>,
pub base_peak_intensity: Option<f64>,
}
pub fn decode_scan(scan_bytes: &[u8]) -> crate::Result<QtflSpectrum> {
if scan_bytes.len() < SCAN_HEADER_SIZE {
return Err(crate::Error::Parse(format!(
"QTOF centroid scan too short for {SCAN_HEADER_SIZE}-byte header: {} bytes",
scan_bytes.len()
)));
}
let bpi = LittleEndian::read_u32(&scan_bytes[0x10..0x14]);
let payload_size = LittleEndian::read_u32(&scan_bytes[0x18..0x1C]) as usize;
let intensity_width = LittleEndian::read_u32(&scan_bytes[0x24..0x28]) as usize;
let payload = &scan_bytes[SCAN_HEADER_SIZE..];
if payload_size == 0 {
return Ok(QtflSpectrum::default());
}
if payload.len() < payload_size {
return Err(crate::Error::Parse(format!(
"QTOF centroid scan payload shorter than declared size: have {}, want {payload_size}",
payload.len()
)));
}
if !matches!(intensity_width, 1 | 2 | 4) {
return Err(crate::Error::Parse(format!(
"QTOF centroid scan has unsupported intensity width {intensity_width} (expected 1, 2, or 4 bytes)"
)));
}
let record_size = 8 + intensity_width;
if payload_size % record_size != 0 {
return Err(crate::Error::Parse(format!(
"QTOF centroid payload size {payload_size} is not a multiple of {record_size} (8-byte mz + {intensity_width}-byte intensity)"
)));
}
let n = payload_size / record_size;
let mz_bytes = &payload[0..n * 8];
let intensity_bytes = &payload[n * 8..n * 8 + n * intensity_width];
let mut mz = Vec::with_capacity(n);
for i in 0..n {
let raw_mz = LittleEndian::read_u64(&mz_bytes[i * 8..i * 8 + 8]);
mz.push(raw_mz as f64 / 1_000_000_000_000.0);
}
let mut intensity = Vec::with_capacity(n);
for i in 0..n {
let base = i * intensity_width;
let raw_intensity: u32 = match intensity_width {
1 => intensity_bytes[base] as u32,
2 => LittleEndian::read_u16(&intensity_bytes[base..base + 2]) as u32,
4 => LittleEndian::read_u32(&intensity_bytes[base..base + 4]),
_ => unreachable!("checked above"),
};
intensity.push(raw_intensity as f32);
}
Ok(QtflSpectrum {
mz,
intensity,
base_peak_intensity: Some(bpi as f64),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_scan(bpi: u32, mzs_scaled: &[u64], intensities: &[u16]) -> Vec<u8> {
let n = mzs_scaled.len();
assert_eq!(n, intensities.len());
let payload_size = n * 10;
let mut buf = vec![0u8; SCAN_HEADER_SIZE];
LittleEndian::write_u32(&mut buf[0x10..0x14], bpi);
LittleEndian::write_u32(&mut buf[0x18..0x1C], payload_size as u32);
LittleEndian::write_u32(&mut buf[0x24..0x28], 2); for m in mzs_scaled {
buf.extend_from_slice(&m.to_le_bytes());
}
for i in intensities {
buf.extend_from_slice(&i.to_le_bytes());
}
buf
}
fn build_scan_u32_intensity(bpi: u32, mzs_scaled: &[u64], intensities: &[u32]) -> Vec<u8> {
let n = mzs_scaled.len();
assert_eq!(n, intensities.len());
let payload_size = n * 12;
let mut buf = vec![0u8; SCAN_HEADER_SIZE];
LittleEndian::write_u32(&mut buf[0x10..0x14], bpi);
LittleEndian::write_u32(&mut buf[0x18..0x1C], payload_size as u32);
LittleEndian::write_u32(&mut buf[0x24..0x28], 4); for m in mzs_scaled {
buf.extend_from_slice(&m.to_le_bytes());
}
for i in intensities {
buf.extend_from_slice(&i.to_le_bytes());
}
buf
}
#[test]
fn decodes_the_doc_worked_example() {
let scan = build_scan(
6650,
&[533_541_065_063_308, 537_476_384_016_910],
&[2886, 1013],
);
let spec = decode_scan(&scan).expect("decode");
assert_eq!(spec.mz.len(), 2);
assert!((spec.mz[0] - 533.541065063308).abs() < 1e-6);
assert!((spec.mz[1] - 537.47638401691).abs() < 1e-6);
assert_eq!(spec.intensity, vec![2886.0, 1013.0]);
assert_eq!(spec.base_peak_intensity, Some(6650.0));
}
#[test]
fn decodes_a_wide_dynamic_range_scan_with_u32_intensity() {
let scan = build_scan_u32_intensity(
68489,
&[516_753_564_527_092, 732_151_569_984_656],
&[30272, 68489],
);
let spec = decode_scan(&scan).expect("decode");
assert_eq!(spec.intensity, vec![30272.0, 68489.0]);
assert_eq!(spec.base_peak_intensity, Some(68489.0));
}
#[test]
fn empty_scan_decodes_to_no_peaks() {
let scan = build_scan(0, &[], &[]);
let spec = decode_scan(&scan).expect("decode");
assert!(spec.mz.is_empty());
assert!(spec.intensity.is_empty());
}
#[test]
fn centroid_index_parses_offset_and_event_id() {
let mut data = vec![0u8; INDEX_RECORD_SIZE * 2];
LittleEndian::write_u32(&mut data[0..4], 0);
LittleEndian::write_u32(&mut data[20..24], 1); LittleEndian::write_u32(&mut data[24..28], 224);
LittleEndian::write_u32(&mut data[24 + 20..24 + 24], 2); let recs = parse_centroid_index(&data).expect("parse");
assert_eq!(recs.len(), 2);
assert_eq!(recs[0].offset, 0);
assert_eq!(recs[0].event_id, 1);
assert_eq!(recs[1].offset, 224);
assert_eq!(recs[1].event_id, 2);
}
}