use byteorder::{ByteOrder, LittleEndian};
use std::collections::BTreeSet;
pub const DATA_INDEX_ENTRY_SIZE: usize = 64;
pub const SUBSET_SIZE: usize = 16;
pub const SCAN_HEADER_SIZE: usize = 64;
#[derive(Debug, Clone, Copy)]
pub struct DataIndexSubset {
pub entry_i: usize,
pub sub_i: usize,
pub offset: u32,
}
pub fn parse_data_index(data: &[u8]) -> crate::Result<Vec<DataIndexSubset>> {
let n_full_blocks = data.len() / DATA_INDEX_ENTRY_SIZE;
let remainder = data.len() % DATA_INDEX_ENTRY_SIZE;
if remainder != 0 && remainder % SUBSET_SIZE != 0 {
return Err(crate::Error::Parse(format!(
"Data Index stream size {} is not a whole number of {SUBSET_SIZE}-byte subsets",
data.len()
)));
}
let mut subsets = Vec::with_capacity(n_full_blocks * 4 + remainder / SUBSET_SIZE);
let parse_block = |block: &[u8], n_subsets: usize, subsets: &mut Vec<DataIndexSubset>| {
for sub_i in 0..n_subsets {
let sub = &block[sub_i * SUBSET_SIZE..(sub_i + 1) * SUBSET_SIZE];
let offset = LittleEndian::read_u32(&sub[0..4]);
let entry_i = LittleEndian::read_u32(&sub[8..12]) as usize;
subsets.push(DataIndexSubset {
entry_i,
sub_i,
offset,
});
}
};
for block_i in 0..n_full_blocks {
let block = &data[block_i * DATA_INDEX_ENTRY_SIZE..(block_i + 1) * DATA_INDEX_ENTRY_SIZE];
parse_block(block, 4, &mut subsets);
}
if remainder > 0 {
let tail = &data[n_full_blocks * DATA_INDEX_ENTRY_SIZE..];
parse_block(tail, remainder / SUBSET_SIZE, &mut subsets);
}
Ok(subsets)
}
pub fn scan_bounds(subsets: &[DataIndexSubset], ms_raw_data_len: usize) -> Vec<(u32, u32)> {
let sorted_offsets: Vec<u32> = subsets
.iter()
.map(|s| s.offset)
.collect::<BTreeSet<u32>>()
.into_iter()
.collect();
let next_of = |offset: u32| -> u32 {
match sorted_offsets.binary_search(&offset) {
Ok(idx) if idx + 1 < sorted_offsets.len() => sorted_offsets[idx + 1],
_ => ms_raw_data_len as u32,
}
};
subsets
.iter()
.map(|s| (s.offset, next_of(s.offset)))
.collect()
}
#[derive(Debug, Clone)]
struct Run {
skip: u16,
values: Vec<u16>,
}
const PREFIX_SEARCH_MAX_RUN_LEN: u16 = 64;
const DECODE_MAX_RUN_LEN: u16 = 4096;
const RLE_MARKER_BASE: u16 = 0x8000;
fn decode_rle(words: &[u16], start: usize, max_run_len: u16) -> Option<(Vec<Run>, usize)> {
let mut i = start;
let mut runs = Vec::new();
let n = words.len();
while i < n {
let w = words[i];
if w == RLE_MARKER_BASE {
return Some((runs, i + 1));
}
if w > RLE_MARKER_BASE && w <= RLE_MARKER_BASE + max_run_len {
let run_len = (w - RLE_MARKER_BASE) as usize;
if i + 2 + run_len > n {
return None; }
let skip = words[i + 1];
let values = words[i + 2..i + 2 + run_len].to_vec();
runs.push(Run { skip, values });
i += 2 + run_len;
} else {
return None; }
}
None }
fn to_u16_words(bytes: &[u8]) -> Vec<u16> {
let nwords = bytes.len() / 2;
(0..nwords)
.map(|w| LittleEndian::read_u16(&bytes[w * 2..w * 2 + 2]))
.collect()
}
fn find_prefix_end(payload: &[u8]) -> Option<usize> {
if payload.len() < 4 {
return None;
}
let mut i = 0;
while i + 1 < payload.len() {
if payload[i + 1] == 0x80 {
let n = payload[i] as u16;
if n <= PREFIX_SEARCH_MAX_RUN_LEN {
let tail = &payload[i..];
if tail.len() % 2 == 0 {
let words = to_u16_words(tail);
let nwords = words.len();
if let Some((_, end_idx)) = decode_rle(&words, 0, DECODE_MAX_RUN_LEN) {
if end_idx == nwords {
return Some(i);
}
}
}
}
}
i += 2;
}
None
}
#[derive(Debug, Clone, Default)]
pub struct TtflSpectrum {
pub index_axis: Vec<f64>,
pub intensity: Vec<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Calibration {
pub a: f64,
pub b: f64,
}
impl Calibration {
pub fn mz(&self, index: f64) -> f64 {
((index - self.b) / self.a).powi(2)
}
}
const TUNING_MASS_OFFSET: usize = 3022;
const TUNING_MASS_STRIDE: usize = 4;
const TUNING_MASS_SCALE: f64 = 1.0e-4;
const TUNING_MASS_MAX_POINTS: usize = 9;
const TUNING_TIME_OFFSET: usize = 3150;
const TUNING_TIME_STRIDE: usize = 8;
const TUNING_MIN_POINTS: usize = 3;
pub fn parse_calibration(data: &[u8]) -> Option<Calibration> {
let mut masses = Vec::new();
for i in 0..TUNING_MASS_MAX_POINTS {
let off = TUNING_MASS_OFFSET + i * TUNING_MASS_STRIDE;
if off + 4 > data.len() {
break;
}
let raw = LittleEndian::read_u32(&data[off..off + 4]);
if raw == 0 {
break; }
masses.push(raw as f64 * TUNING_MASS_SCALE);
}
if masses.len() < TUNING_MIN_POINTS {
return None;
}
let mut times = Vec::with_capacity(masses.len());
for i in 0..masses.len() {
let off = TUNING_TIME_OFFSET + i * TUNING_TIME_STRIDE;
if off + 8 > data.len() {
return None;
}
times.push(LittleEndian::read_f64(&data[off..off + 8]));
}
fit_sqrt_linear(&masses, ×)
}
fn fit_sqrt_linear(masses: &[f64], times: &[f64]) -> Option<Calibration> {
debug_assert_eq!(masses.len(), times.len());
let n = masses.len() as f64;
let xs: Vec<f64> = masses.iter().map(|m| m.sqrt()).collect();
let sx: f64 = xs.iter().sum();
let sy: f64 = times.iter().sum();
let sxx: f64 = xs.iter().map(|x| x * x).sum();
let sxy: f64 = xs.iter().zip(times).map(|(x, y)| x * y).sum();
let denom = n * sxx - sx * sx;
if denom.abs() < 1e-9 {
return None;
}
let a = (n * sxy - sx * sy) / denom;
let b = (sy - a * sx) / n;
if !a.is_finite() || !b.is_finite() || a <= 0.0 {
return None;
}
Some(Calibration { a, b })
}
pub fn decode_scan(scan_bytes: &[u8]) -> Option<TtflSpectrum> {
if scan_bytes.len() < SCAN_HEADER_SIZE {
return None;
}
let payload = &scan_bytes[SCAN_HEADER_SIZE..];
if payload.is_empty() {
return Some(TtflSpectrum::default());
}
let prefix_end = find_prefix_end(payload)?;
let tail = &payload[prefix_end..];
if tail.len() % 2 != 0 {
return None;
}
let words = to_u16_words(tail);
let nwords = words.len();
let (runs, end_idx) = decode_rle(&words, 0, DECODE_MAX_RUN_LEN)?;
if end_idx != nwords {
return None; }
let mut index_axis = Vec::new();
let mut intensity = Vec::new();
let mut pos: u32 = 0;
for run in runs {
pos += run.skip as u32;
for v in run.values {
index_axis.push(pos as f64);
intensity.push(v as f32);
pos += 1;
}
}
Some(TtflSpectrum {
index_axis,
intensity,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn write_subset(buf: &mut [u8], offset: u32, entry_i: u32, evctr: u32) {
LittleEndian::write_u32(&mut buf[0..4], offset);
LittleEndian::write_u32(&mut buf[4..8], 0);
LittleEndian::write_u32(&mut buf[8..12], entry_i);
LittleEndian::write_u32(&mut buf[12..16], evctr);
}
#[test]
fn parse_data_index_reads_entry_i_from_bytes_not_position() {
let mut block = vec![0u8; 64];
write_subset(&mut block[0..16], 100, 0, 0);
write_subset(&mut block[16..32], 200, 0, 1);
write_subset(&mut block[32..48], 300, 1, 2);
write_subset(&mut block[48..64], 400, 1, 3);
let subsets = parse_data_index(&block).expect("parse");
assert_eq!(subsets.len(), 4);
assert_eq!(subsets[0].entry_i, 0);
assert_eq!(subsets[1].entry_i, 0);
assert_eq!(subsets[2].entry_i, 1);
assert_eq!(subsets[3].entry_i, 1);
}
#[test]
fn parse_data_index_accepts_trailing_partial_block() {
let mut data = vec![0u8; 64 + 32];
write_subset(&mut data[0..16], 0, 0, 0);
write_subset(&mut data[16..32], 10, 0, 1);
write_subset(&mut data[32..48], 20, 0, 2);
write_subset(&mut data[48..64], 30, 0, 3);
write_subset(&mut data[64..80], 40, 1, 4);
write_subset(&mut data[80..96], 50, 1, 5);
let subsets = parse_data_index(&data).expect("parse");
assert_eq!(subsets.len(), 6);
assert_eq!(subsets[4].entry_i, 1);
assert_eq!(subsets[4].offset, 40);
assert_eq!(subsets[5].entry_i, 1);
assert_eq!(subsets[5].offset, 50);
}
#[test]
fn parse_data_index_rejects_non_16_byte_remainder() {
let data = vec![0u8; 64 + 7]; assert!(parse_data_index(&data).is_err());
}
#[test]
fn decodes_a_minimal_rle_tail() {
let mut scan = vec![0u8; SCAN_HEADER_SIZE];
scan.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
scan.extend_from_slice(&0x8002u16.to_le_bytes());
scan.extend_from_slice(&263u16.to_le_bytes());
scan.extend_from_slice(&289u16.to_le_bytes());
scan.extend_from_slice(&67u16.to_le_bytes());
scan.extend_from_slice(&0x8000u16.to_le_bytes());
let spec = decode_scan(&scan).expect("decode");
assert_eq!(spec.index_axis, vec![263.0, 264.0]);
assert_eq!(spec.intensity, vec![289.0, 67.0]);
}
#[test]
fn empty_payload_decodes_to_empty_spectrum() {
let scan = vec![0u8; SCAN_HEADER_SIZE];
let spec = decode_scan(&scan).expect("decode");
assert!(spec.index_axis.is_empty());
assert!(spec.intensity.is_empty());
}
#[test]
fn malformed_payload_is_skipped() {
let mut scan = vec![0u8; SCAN_HEADER_SIZE];
scan.extend_from_slice(&[1, 2, 3, 4, 5, 6]);
assert!(decode_scan(&scan).is_none());
}
fn make_tuning_result_stream(masses: &[f64], times: &[f64]) -> Vec<u8> {
assert_eq!(masses.len(), times.len());
let mut buf = vec![0u8; TUNING_TIME_OFFSET + times.len() * TUNING_TIME_STRIDE];
for (i, &m) in masses.iter().enumerate() {
let off = TUNING_MASS_OFFSET + i * TUNING_MASS_STRIDE;
let raw = (m / TUNING_MASS_SCALE).round() as u32;
LittleEndian::write_u32(&mut buf[off..off + 4], raw);
}
for (i, &t) in times.iter().enumerate() {
let off = TUNING_TIME_OFFSET + i * TUNING_TIME_STRIDE;
LittleEndian::write_f64(&mut buf[off..off + 8], t);
}
buf
}
const MTBLS432_MASSES: [f64; 9] = [
1589.64, 2949.388, 4309.136, 5668.885, 7028.633, 8388.381, 9748.129, 11107.877, 12467.625,
];
const MTBLS432_TIMES: [f64; 9] = [
21908.68359375,
29806.38671875,
36007.29296875001,
41284.74218750001,
45958.96484375,
50198.98046875,
54107.10156249999,
57750.941406250015,
61177.76953125,
];
#[test]
fn fit_sqrt_linear_recovers_known_corpus_calibration() {
let cal = fit_sqrt_linear(&MTBLS432_MASSES, &MTBLS432_TIMES).expect("fit");
assert!(
(cal.a - 547.012885).abs() < 1e-4,
"a = {} not close to 547.012885",
cal.a
);
assert!(
(cal.b - 99.101660).abs() < 1e-4,
"b = {} not close to 99.101660",
cal.b
);
}
#[test]
fn fit_sqrt_linear_residuals_are_near_zero() {
let cal = fit_sqrt_linear(&MTBLS432_MASSES, &MTBLS432_TIMES).expect("fit");
for (&m, &t) in MTBLS432_MASSES.iter().zip(MTBLS432_TIMES.iter()) {
let predicted_t = cal.a * m.sqrt() + cal.b;
assert!(
(predicted_t - t).abs() < 0.1,
"residual too large at mass {m}: predicted {predicted_t}, actual {t}"
);
}
}
#[test]
fn calibration_mz_inverts_the_fit() {
let cal = fit_sqrt_linear(&MTBLS432_MASSES, &MTBLS432_TIMES).expect("fit");
for (&m, &t) in MTBLS432_MASSES.iter().zip(MTBLS432_TIMES.iter()) {
let recovered_mz = cal.mz(t);
assert!(
(recovered_mz - m).abs() < 0.05,
"mz({t}) = {recovered_mz}, expected ~{m}"
);
}
}
#[test]
fn parse_calibration_reads_real_layout() {
let stream = make_tuning_result_stream(&MTBLS432_MASSES, &MTBLS432_TIMES);
let cal = parse_calibration(&stream).expect("parse_calibration");
assert!((cal.a - 547.012885).abs() < 1e-3);
assert!((cal.b - 99.101660).abs() < 1e-3);
}
#[test]
fn parse_calibration_stops_at_zero_padding() {
let masses = &MTBLS432_MASSES[..5];
let times = &MTBLS432_TIMES[..5];
let stream = make_tuning_result_stream(masses, times);
let cal = parse_calibration(&stream).expect("parse_calibration");
assert!((cal.a - 547.012885).abs() < 1.0);
assert!((cal.b - 99.101660).abs() < 5.0);
}
#[test]
fn parse_calibration_returns_none_below_min_points() {
let masses = &MTBLS432_MASSES[..2];
let times = &MTBLS432_TIMES[..2];
let stream = make_tuning_result_stream(masses, times);
assert!(parse_calibration(&stream).is_none());
}
#[test]
fn parse_calibration_returns_none_for_short_stream() {
assert!(parse_calibration(&[0u8; 16]).is_none());
}
#[test]
fn parse_calibration_returns_none_for_all_zero_stream() {
let stream = vec![0u8; TUNING_TIME_OFFSET + 9 * TUNING_TIME_STRIDE];
assert!(parse_calibration(&stream).is_none());
}
#[test]
fn scan_bounds_uses_global_sorted_offsets() {
let subsets = vec![
DataIndexSubset {
entry_i: 0,
sub_i: 0,
offset: 0,
},
DataIndexSubset {
entry_i: 0,
sub_i: 1,
offset: 50,
},
DataIndexSubset {
entry_i: 1,
sub_i: 0,
offset: 20,
},
DataIndexSubset {
entry_i: 1,
sub_i: 1,
offset: 80,
},
];
let bounds = scan_bounds(&subsets, 100);
assert_eq!(bounds[0], (0, 20));
assert_eq!(bounds[1], (50, 80));
assert_eq!(bounds[2], (20, 50));
assert_eq!(bounds[3], (80, 100));
}
}