Skip to main content

rustdf/data/
handle.rs

1use crate::data::meta::{read_global_meta_sql, read_meta_data_sql, FrameMeta, GlobalMetaData};
2use crate::data::raw::BrukerTimsDataLibrary;
3use crate::data::utility::{
4    flatten_scan_values, parse_decompressed_bruker_binary_data, zstd_decompress,
5};
6use byteorder::{LittleEndian, ReadBytesExt};
7use mscore::data::spectrum::MsType;
8use mscore::timstof::frame::{ImsFrame, RawTimsFrame, TimsFrame};
9use mscore::timstof::slice::TimsSlice;
10use std::fs::File;
11use std::io::{Cursor, Read, Seek, SeekFrom};
12use std::path::PathBuf;
13
14use crate::data::acquisition::AcquisitionMode;
15use rayon::prelude::*;
16use rayon::ThreadPoolBuilder;
17
18use std::error::Error;
19
20/// Derive m/z calibration coefficients by using the Bruker SDK.
21///
22/// This function temporarily loads the SDK to convert a range of TOF indices to m/z,
23/// then fits a linear regression to derive accurate calibration coefficients.
24///
25/// Returns `Some((intercept, slope))` for the formula: `sqrt(mz) = intercept + slope * tof`
26/// Returns `None` if SDK is not available (e.g., on macOS).
27fn derive_mz_calibration(
28    bruker_lib_path: &str,
29    data_path: &str,
30    tof_max_index: u32,
31) -> Option<(f64, f64)> {
32    // Check if SDK path is valid before trying to load
33    // Common invalid values: "NO_SDK", empty string, or non-existent paths
34    if bruker_lib_path.is_empty()
35        || bruker_lib_path == "NO_SDK"
36        || bruker_lib_path == "CALIBRATED"
37        || !std::path::Path::new(bruker_lib_path).exists()
38    {
39        return None;
40    }
41
42    // Try to create a BrukerLib converter. Use the FALLIBLE `try_new`, not `catch_unwind` around the
43    // panicking `new`: the workspace builds release with `panic = "abort"`, so catch_unwind can't
44    // recover — a present-but-unloadable SDK (wrong arch / mismatched version) would abort instead of
45    // returning None. try_new returns Err on load failure with no panic.
46    let sdk_converter = match BrukerLibTimsDataConverter::try_new(bruker_lib_path, data_path) {
47        Ok(converter) => converter,
48        Err(_) => return None,
49    };
50
51    // Generate a range of TOF indices across the spectrum
52    let n_points = 1000;
53    let step = tof_max_index / n_points;
54    let tof_indices: Vec<u32> = (0..n_points).map(|i| i * step + step / 2).collect();
55
56    // Convert to m/z using SDK
57    let mz_values = sdk_converter.tof_to_mz(1, &tof_indices);
58
59    // Filter out any invalid values (zero or negative)
60    let valid_pairs: Vec<(f64, f64)> = tof_indices
61        .iter()
62        .zip(mz_values.iter())
63        .filter(|(_, mz)| **mz > 0.0)
64        .map(|(tof, mz)| (*tof as f64, mz.sqrt()))
65        .collect();
66
67    if valid_pairs.len() < 10 {
68        return None;
69    }
70
71    // Linear regression: sqrt(mz) = intercept + slope * tof
72    // Using simple least squares: slope = Cov(x,y) / Var(x), intercept = mean_y - slope * mean_x
73    let n = valid_pairs.len() as f64;
74    let sum_x: f64 = valid_pairs.iter().map(|(x, _)| x).sum();
75    let sum_y: f64 = valid_pairs.iter().map(|(_, y)| y).sum();
76    let sum_xy: f64 = valid_pairs.iter().map(|(x, y)| x * y).sum();
77    let sum_xx: f64 = valid_pairs.iter().map(|(x, _)| x * x).sum();
78
79    let mean_x = sum_x / n;
80    let mean_y = sum_y / n;
81
82    let slope = (sum_xy - n * mean_x * mean_y) / (sum_xx - n * mean_x * mean_x);
83    let intercept = mean_y - slope * mean_x;
84
85    Some((intercept, slope))
86}
87
88/// Build a `LookupIndexConverter`, preferring an SDK-derived m/z calibration.
89///
90/// The `LookupIndexConverter` always carries the pre-computed scan→1/K0 lookup
91/// for ion mobility. For m/z it normally uses a 2-point boundary model
92/// (`LookupIndexConverter::new`), which can have a large m/z error on some
93/// datasets. When a valid `bruker_lib_path` is supplied, m/z is instead
94/// calibrated with the same regression fit used by the `Calibrated` converter
95/// (`derive_mz_calibration`). Falls back to the boundary model when the SDK is
96/// unavailable (e.g. macOS, or `bruker_lib_path` is empty / "NO_SDK").
97fn build_lookup_converter(
98    bruker_lib_path: &str,
99    data_path: &str,
100    tof_max_index: u32,
101    mz_lower: f64,
102    mz_upper: f64,
103    im_lookup: Vec<f64>,
104) -> LookupIndexConverter {
105    match derive_mz_calibration(bruker_lib_path, data_path, tof_max_index) {
106        Some((intercept, slope)) => {
107            LookupIndexConverter::with_mz_fit(intercept, slope, im_lookup)
108        }
109        None => {
110            if !bruker_lib_path.is_empty() && bruker_lib_path != "NO_SDK" {
111                eprintln!(
112                    "Warning: Could not derive m/z calibration from SDK at '{}'. \
113                    Falling back to the 2-point boundary model, which may have a \
114                    large m/z error on some datasets.",
115                    bruker_lib_path
116                );
117            }
118            LookupIndexConverter::new(mz_lower, mz_upper, tof_max_index, im_lookup)
119        }
120    }
121}
122
123fn lzf_decompress(data: &[u8], max_output_size: usize) -> Result<Vec<u8>, Box<dyn Error>> {
124    let decompressed_data = lzf::decompress(data, max_output_size)
125        .map_err(|e| format!("LZF decompression failed: {}", e))?;
126    Ok(decompressed_data)
127}
128
129fn parse_decompressed_bruker_binary_type1(
130    decompressed_bytes: &[u8],
131    scan_indices: &mut [i64],
132    tof_indices: &mut [u32],
133    intensities: &mut [u16],
134    scan_start: usize,
135    scan_index: usize,
136) -> usize {
137    // Interpret decompressed_bytes as a slice of i32
138    let int_count = decompressed_bytes.len() / 4;
139    let buffer =
140        unsafe { std::slice::from_raw_parts(decompressed_bytes.as_ptr() as *const i32, int_count) };
141
142    let mut tof_index = 0i32;
143    let mut previous_was_intensity = true;
144    let mut current_index = scan_start;
145
146    for &value in buffer {
147        if value >= 0 {
148            // positive value => intensity
149            if previous_was_intensity {
150                tof_index += 1;
151            }
152            tof_indices[current_index] = tof_index as u32;
153            intensities[current_index] = value as u16;
154            previous_was_intensity = true;
155            current_index += 1;
156        } else {
157            // negative value => indicates a jump in tof_index
158            tof_index -= value; // value is negative, so this adds |value| to tof_index
159            previous_was_intensity = false;
160        }
161    }
162
163    let scan_size = current_index - scan_start;
164    scan_indices[scan_index] = scan_size as i64;
165    scan_size
166}
167
168pub struct TimsRawDataLayout {
169    pub raw_data_path: String,
170    pub global_meta_data: GlobalMetaData,
171    pub frame_meta_data: Vec<FrameMeta>,
172    pub max_scan_count: i64,
173    pub frame_id_ptr: Vec<i64>,
174    pub tims_offset_values: Vec<i64>,
175    pub acquisition_mode: AcquisitionMode,
176}
177
178impl TimsRawDataLayout {
179    pub fn new(data_path: &str) -> Self {
180        // get the global and frame meta data
181        let global_meta_data = read_global_meta_sql(data_path).unwrap();
182        let frame_meta_data = read_meta_data_sql(data_path).unwrap();
183
184        // get the max scan count
185        let max_scan_count = frame_meta_data.iter().map(|x| x.num_scans).max().unwrap();
186
187        let mut frame_id_ptr: Vec<i64> = Vec::new();
188        frame_id_ptr.resize(frame_meta_data.len() + 1, 0);
189
190        // get the frame id_ptr values
191        for (i, row) in frame_meta_data.iter().enumerate() {
192            frame_id_ptr[i + 1] = row.num_peaks + frame_id_ptr[i];
193        }
194
195        // get the tims offset values
196        let tims_offset_values = frame_meta_data
197            .iter()
198            .map(|x| x.tims_id)
199            .collect::<Vec<i64>>();
200
201        // get the acquisition mode
202        let acquisition_mode = match frame_meta_data[0].scan_mode {
203            8 => AcquisitionMode::DDA,
204            9 => AcquisitionMode::DIA,
205            _ => AcquisitionMode::Unknown,
206        };
207
208        TimsRawDataLayout {
209            raw_data_path: data_path.to_string(),
210            global_meta_data,
211            frame_meta_data,
212            max_scan_count,
213            frame_id_ptr,
214            tims_offset_values,
215            acquisition_mode,
216        }
217    }
218}
219
220pub trait TimsData {
221    fn get_frame(&self, frame_id: u32) -> TimsFrame;
222    fn get_raw_frame(&self, frame_id: u32) -> RawTimsFrame;
223    fn get_slice(&self, frame_ids: Vec<u32>, num_threads: usize) -> TimsSlice;
224    fn get_acquisition_mode(&self) -> AcquisitionMode;
225    fn get_frame_count(&self) -> i32;
226    fn get_data_path(&self) -> &str;
227}
228
229pub trait IndexConverter {
230    fn tof_to_mz(&self, frame_id: u32, tof_values: &Vec<u32>) -> Vec<f64>;
231    fn mz_to_tof(&self, frame_id: u32, mz_values: &Vec<f64>) -> Vec<u32>;
232    fn scan_to_inverse_mobility(&self, frame_id: u32, scan_values: &Vec<u32>) -> Vec<f64>;
233    fn inverse_mobility_to_scan(
234        &self,
235        frame_id: u32,
236        inverse_mobility_values: &Vec<f64>,
237    ) -> Vec<u32>;
238}
239
240pub struct BrukerLibTimsDataConverter {
241    pub bruker_lib: BrukerTimsDataLibrary,
242}
243
244impl BrukerLibTimsDataConverter {
245    pub fn new(bruker_lib_path: &str, data_path: &str) -> Self {
246        Self::try_new(bruker_lib_path, data_path).unwrap()
247    }
248    /// Fallible constructor — `Err` if the Bruker SDK shared library can't be loaded (missing,
249    /// wrong architecture, mismatched version). Lets callers degrade to the simple/derived
250    /// calibration instead of panicking.
251    pub fn try_new(
252        bruker_lib_path: &str,
253        data_path: &str,
254    ) -> Result<Self, Box<dyn std::error::Error>> {
255        let bruker_lib = BrukerTimsDataLibrary::new(bruker_lib_path, data_path)?;
256        Ok(BrukerLibTimsDataConverter { bruker_lib })
257    }
258}
259impl IndexConverter for BrukerLibTimsDataConverter {
260    /// translate tof to mz values calling the bruker library
261    ///
262    /// # Arguments
263    ///
264    /// * `frame_id` - A u32 that holds the frame id
265    /// * `tof` - A vector of u32 that holds the tof values
266    ///
267    /// # Returns
268    ///
269    /// * `mz_values` - A vector of f64 that holds the mz values
270    ///
271    fn tof_to_mz(&self, frame_id: u32, tof: &Vec<u32>) -> Vec<f64> {
272        let mut dbl_tofs: Vec<f64> = Vec::new();
273        dbl_tofs.resize(tof.len(), 0.0);
274
275        for (i, &val) in tof.iter().enumerate() {
276            dbl_tofs[i] = val as f64;
277        }
278
279        let mut mz_values: Vec<f64> = Vec::new();
280        mz_values.resize(tof.len(), 0.0);
281
282        self.bruker_lib
283            .tims_index_to_mz(frame_id, &dbl_tofs, &mut mz_values)
284            .expect("Bruker binary call failed at: tims_index_to_mz;");
285
286        mz_values
287    }
288
289    fn mz_to_tof(&self, frame_id: u32, mz: &Vec<f64>) -> Vec<u32> {
290        let mut dbl_mz: Vec<f64> = Vec::new();
291        dbl_mz.resize(mz.len(), 0.0);
292
293        for (i, &val) in mz.iter().enumerate() {
294            dbl_mz[i] = val;
295        }
296
297        let mut tof_values: Vec<f64> = Vec::new();
298        tof_values.resize(mz.len(), 0.0);
299
300        self.bruker_lib
301            .tims_mz_to_index(frame_id, &dbl_mz, &mut tof_values)
302            .expect("Bruker binary call failed at: tims_mz_to_index;");
303
304        tof_values.iter().map(|&x| x.round() as u32).collect()
305    }
306
307    /// translate scan to inverse mobility values calling the bruker library
308    ///
309    /// # Arguments
310    ///
311    /// * `frame_id` - A u32 that holds the frame id
312    /// * `scan` - A vector of i32 that holds the scan values
313    ///
314    /// # Returns
315    ///
316    /// * `inv_mob` - A vector of f64 that holds the inverse mobility values
317    ///
318    fn scan_to_inverse_mobility(&self, frame_id: u32, scan: &Vec<u32>) -> Vec<f64> {
319        let mut dbl_scans: Vec<f64> = Vec::new();
320        dbl_scans.resize(scan.len(), 0.0);
321
322        for (i, &val) in scan.iter().enumerate() {
323            dbl_scans[i] = val as f64;
324        }
325
326        let mut inv_mob: Vec<f64> = Vec::new();
327        inv_mob.resize(scan.len(), 0.0);
328
329        self.bruker_lib
330            .tims_scan_to_inv_mob(frame_id, &dbl_scans, &mut inv_mob)
331            .expect("Bruker binary call failed at: tims_scannum_to_oneoverk0;");
332
333        inv_mob
334    }
335
336    /// translate inverse mobility to scan values calling the bruker library
337    ///
338    /// # Arguments
339    ///
340    /// * `frame_id` - A u32 that holds the frame id
341    /// * `inv_mob` - A vector of f64 that holds the inverse mobility values
342    ///
343    /// # Returns
344    ///
345    /// * `scan_values` - A vector of i32 that holds the scan values
346    ///
347    fn inverse_mobility_to_scan(&self, frame_id: u32, inv_mob: &Vec<f64>) -> Vec<u32> {
348        let mut dbl_inv_mob: Vec<f64> = Vec::new();
349        dbl_inv_mob.resize(inv_mob.len(), 0.0);
350
351        for (i, &val) in inv_mob.iter().enumerate() {
352            dbl_inv_mob[i] = val;
353        }
354
355        let mut scan_values: Vec<f64> = Vec::new();
356        scan_values.resize(inv_mob.len(), 0.0);
357
358        self.bruker_lib
359            .inv_mob_to_tims_scan(frame_id, &dbl_inv_mob, &mut scan_values)
360            .expect("Bruker binary call failed at: tims_oneoverk0_to_scannum;");
361
362        scan_values.iter().map(|&x| x.round() as u32).collect()
363    }
364}
365
366/// SDK-free converter using the exact Bruker calibration formulas.
367///
368/// Reads the `MzCalibration` and `TimsCalibration` tables from analysis.tdf and
369/// evaluates the published calibration curves directly (see
370/// [`crate::data::calibration`]). Verified against the Bruker SDK:
371///   * scan <-> 1/K0 : machine-precision exact (ModelType 2).
372///   * TOF  <-> m/z  : bit-exact for MzCalibration ModelType 1; ModelType 2 uses
373///     the same C0/C1/C2 quadratic-in-sqrt(m) curve and reproduces the SDK to a
374///     few ppm (the proprietary ModelType-2 C8..C14 fine correction is not
375///     modelled).
376///
377/// This is a **fixed-calibration** converter: it captures one frame's
378/// calibration rows + temperatures at build time and applies them to every
379/// frame, ignoring the per-call `frame_id` (like the other SDK-free converters
380/// `Simple`/`Calibrated`/`Lookup`). Valid because the coefficients are
381/// effectively constant across a run; use the SDK-backed `BrukerLib` converter
382/// if a run genuinely carries multiple calibration rows.
383pub struct BrukerFormulaConverter {
384    pub mz: crate::data::calibration::MzCalibrator,
385    pub im: crate::data::calibration::MobilityCalibrator,
386    pub mz_model_type: i64,
387}
388
389impl BrukerFormulaConverter {
390    /// Build the converter from a `.d` folder, using `frame_id`'s calibration
391    /// row and per-frame temperatures (coefficients are near-constant per run).
392    pub fn from_d_folder(
393        data_path: &str,
394        frame_id: u32,
395    ) -> Result<Self, Box<dyn std::error::Error>> {
396        use crate::data::calibration::{MobilityCalibrator, MzCalibrator};
397        use crate::data::meta::{read_mz_calibration, read_tims_calibration};
398
399        let tdf = PathBuf::from(data_path).join("analysis.tdf");
400        let con = rusqlite::Connection::open(&tdf)?;
401        let (t1, t2, mz_id, tims_id): (f64, f64, i64, i64) = con.query_row(
402            "SELECT T1, T2, MzCalibration, TimsCalibration FROM Frames WHERE Id = ?1",
403            [frame_id],
404            |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
405        )?;
406
407        let mzc = read_mz_calibration(data_path)?
408            .into_iter()
409            .find(|c| c.id == mz_id)
410            .ok_or("MzCalibration row not found")?;
411        let tc = read_tims_calibration(data_path)?
412            .into_iter()
413            .find(|c| c.id == tims_id)
414            .ok_or("TimsCalibration row not found")?;
415
416        // Reject calibration models this converter does not implement, rather
417        // than silently mis-computing (m/z model 2 is the default branch).
418        if mzc.model_type != 1 && mzc.model_type != 2 {
419            return Err(format!("unsupported MzCalibration ModelType {}", mzc.model_type).into());
420        }
421        if tc.model_type != 2 {
422            return Err(format!("unsupported TimsCalibration ModelType {}", tc.model_type).into());
423        }
424
425        let mz = MzCalibrator::new(
426            mzc.model_type,
427            mzc.digitizer_timebase,
428            mzc.digitizer_delay,
429            mzc.t1,
430            mzc.t2,
431            mzc.dc1,
432            mzc.dc2,
433            mzc.c0,
434            mzc.c1,
435            mzc.c2,
436            mzc.c3,
437            mzc.c4,
438            t1,
439            t2,
440        );
441        let im = MobilityCalibrator::new(
442            tc.c0, tc.c1, tc.c2, tc.c3, tc.c4, tc.c5, tc.c6, tc.c7, tc.c8, tc.c9,
443        );
444        Ok(Self { mz, im, mz_model_type: mzc.model_type })
445    }
446}
447
448impl IndexConverter for BrukerFormulaConverter {
449    fn tof_to_mz(&self, _frame_id: u32, tof_values: &Vec<u32>) -> Vec<f64> {
450        tof_values.iter().map(|&t| self.mz.tof_to_mz(t)).collect()
451    }
452    fn mz_to_tof(&self, _frame_id: u32, mz_values: &Vec<f64>) -> Vec<u32> {
453        mz_values.iter().map(|&m| self.mz.mz_to_tof(m)).collect()
454    }
455    fn scan_to_inverse_mobility(&self, _frame_id: u32, scan_values: &Vec<u32>) -> Vec<f64> {
456        scan_values
457            .iter()
458            .map(|&s| self.im.scan_to_one_over_k0(s))
459            .collect()
460    }
461    fn inverse_mobility_to_scan(
462        &self,
463        _frame_id: u32,
464        inverse_mobility_values: &Vec<f64>,
465    ) -> Vec<u32> {
466        inverse_mobility_values
467            .iter()
468            .map(|&v| self.im.one_over_k0_to_scan(v))
469            .collect()
470    }
471}
472
473pub enum TimsIndexConverter {
474    Simple(SimpleIndexConverter),
475    Calibrated(CalibratedIndexConverter),
476    BrukerLib(BrukerLibTimsDataConverter),
477    Lookup(LookupIndexConverter),
478    BrukerFormula(BrukerFormulaConverter),
479}
480
481impl IndexConverter for TimsIndexConverter {
482    fn tof_to_mz(&self, frame_id: u32, tof_values: &Vec<u32>) -> Vec<f64> {
483        match self {
484            TimsIndexConverter::Simple(converter) => converter.tof_to_mz(frame_id, tof_values),
485            TimsIndexConverter::Calibrated(converter) => converter.tof_to_mz(frame_id, tof_values),
486            TimsIndexConverter::BrukerLib(converter) => converter.tof_to_mz(frame_id, tof_values),
487            TimsIndexConverter::Lookup(converter) => converter.tof_to_mz(frame_id, tof_values),
488            TimsIndexConverter::BrukerFormula(converter) => converter.tof_to_mz(frame_id, tof_values),
489        }
490    }
491
492    fn mz_to_tof(&self, frame_id: u32, mz_values: &Vec<f64>) -> Vec<u32> {
493        match self {
494            TimsIndexConverter::Simple(converter) => converter.mz_to_tof(frame_id, mz_values),
495            TimsIndexConverter::Calibrated(converter) => converter.mz_to_tof(frame_id, mz_values),
496            TimsIndexConverter::BrukerLib(converter) => converter.mz_to_tof(frame_id, mz_values),
497            TimsIndexConverter::Lookup(converter) => converter.mz_to_tof(frame_id, mz_values),
498            TimsIndexConverter::BrukerFormula(converter) => converter.mz_to_tof(frame_id, mz_values),
499        }
500    }
501
502    fn scan_to_inverse_mobility(&self, frame_id: u32, scan_values: &Vec<u32>) -> Vec<f64> {
503        match self {
504            TimsIndexConverter::Simple(converter) => {
505                converter.scan_to_inverse_mobility(frame_id, scan_values)
506            }
507            TimsIndexConverter::Calibrated(converter) => {
508                converter.scan_to_inverse_mobility(frame_id, scan_values)
509            }
510            TimsIndexConverter::BrukerLib(converter) => {
511                converter.scan_to_inverse_mobility(frame_id, scan_values)
512            }
513            TimsIndexConverter::Lookup(converter) => {
514                converter.scan_to_inverse_mobility(frame_id, scan_values)
515            }
516            TimsIndexConverter::BrukerFormula(converter) => {
517                converter.scan_to_inverse_mobility(frame_id, scan_values)
518            }
519        }
520    }
521
522    fn inverse_mobility_to_scan(
523        &self,
524        frame_id: u32,
525        inverse_mobility_values: &Vec<f64>,
526    ) -> Vec<u32> {
527        match self {
528            TimsIndexConverter::Simple(converter) => {
529                converter.inverse_mobility_to_scan(frame_id, inverse_mobility_values)
530            }
531            TimsIndexConverter::Calibrated(converter) => {
532                converter.inverse_mobility_to_scan(frame_id, inverse_mobility_values)
533            }
534            TimsIndexConverter::BrukerLib(converter) => {
535                converter.inverse_mobility_to_scan(frame_id, inverse_mobility_values)
536            }
537            TimsIndexConverter::Lookup(converter) => {
538                converter.inverse_mobility_to_scan(frame_id, inverse_mobility_values)
539            }
540            TimsIndexConverter::BrukerFormula(converter) => {
541                converter.inverse_mobility_to_scan(frame_id, inverse_mobility_values)
542            }
543        }
544    }
545}
546
547pub struct TimsLazyLoder {
548    pub raw_data_layout: TimsRawDataLayout,
549    pub index_converter: TimsIndexConverter,
550}
551
552impl TimsData for TimsLazyLoder {
553    fn get_frame(&self, frame_id: u32) -> TimsFrame {
554        let frame_index = (frame_id - 1) as usize;
555
556        // turns out, there can be empty frames in the data, check for that, if so, return an empty frame
557        let num_peaks = self.raw_data_layout.frame_meta_data[frame_index].num_peaks;
558
559        if num_peaks == 0 {
560
561            let ms_type_raw = self.raw_data_layout.frame_meta_data[frame_index].ms_ms_type;
562
563            let ms_type = match ms_type_raw {
564                0 => MsType::Precursor,
565                8 => MsType::FragmentDda,
566                9 => MsType::FragmentDia,
567                _ => MsType::Unknown,
568            };
569
570            return TimsFrame {
571                frame_id: frame_id as i32,
572                ms_type,
573                scan: Vec::new(),
574                tof: Vec::new(),
575                ims_frame: ImsFrame::new(
576                    self.raw_data_layout.frame_meta_data[(frame_id - 1) as usize].time,
577                    Vec::new(),
578                    Vec::new(),
579                    Vec::new(),
580                ),
581            };
582        }
583
584        let offset = self.raw_data_layout.tims_offset_values[frame_index] as u64;
585
586        let mut file_path = PathBuf::from(&self.raw_data_layout.raw_data_path);
587        file_path.push("analysis.tdf_bin");
588        let mut infile = File::open(&file_path).unwrap();
589
590        infile.seek(SeekFrom::Start(offset)).unwrap();
591
592        let mut bin_buffer = [0u8; 4];
593        infile.read_exact(&mut bin_buffer).unwrap();
594        let bin_size = Cursor::new(bin_buffer).read_i32::<LittleEndian>().unwrap();
595
596        infile.read_exact(&mut bin_buffer).unwrap();
597
598        match self.raw_data_layout.global_meta_data.tims_compression_type {
599            1 => {
600                let scan_count =
601                    self.raw_data_layout.frame_meta_data[frame_index].num_scans as usize;
602                let num_peaks = num_peaks as usize;
603                let compression_offset = 8 + (scan_count + 1) * 4;
604
605                let mut scan_offsets_buffer = vec![0u8; (scan_count + 1) * 4];
606                infile.read_exact(&mut scan_offsets_buffer).unwrap();
607
608                let mut scan_offsets = Vec::with_capacity(scan_count + 1);
609                {
610                    let mut rdr = Cursor::new(&scan_offsets_buffer);
611                    for _ in 0..(scan_count + 1) {
612                        scan_offsets.push(rdr.read_i32::<LittleEndian>().unwrap());
613                    }
614                }
615
616                for offs in &mut scan_offsets {
617                    *offs -= compression_offset as i32;
618                }
619
620                let remaining_size = (bin_size as usize - compression_offset) as usize;
621                let mut compressed_data = vec![0u8; remaining_size];
622                infile.read_exact(&mut compressed_data).unwrap();
623
624                let mut scan_indices_ = vec![0i64; scan_count];
625                let mut tof_indices_ = vec![0u32; num_peaks];
626                let mut intensities_ = vec![0u16; num_peaks];
627
628                let mut scan_start = 0usize;
629
630                for scan_index in 0..scan_count {
631                    let start = scan_offsets[scan_index] as usize;
632                    let end = scan_offsets[scan_index + 1] as usize;
633
634                    if start == end {
635                        continue;
636                    }
637
638                    let max_output_size = num_peaks * 8;
639                    let decompressed_bytes =
640                        lzf_decompress(&compressed_data[start..end], max_output_size)
641                            .expect("LZF decompression failed.");
642
643                    scan_start += parse_decompressed_bruker_binary_type1(
644                        &decompressed_bytes,
645                        &mut scan_indices_,
646                        &mut tof_indices_,
647                        &mut intensities_,
648                        scan_start,
649                        scan_index,
650                    );
651                }
652
653                // Create a flat scan vector to match what flatten_scan_values expects
654                let mut scan = Vec::with_capacity(num_peaks);
655                {
656                    let mut current_scan_index = 0u32;
657                    for &size in &scan_indices_ {
658                        let sz = size as usize;
659                        for _ in 0..sz {
660                            scan.push(current_scan_index);
661                        }
662                        current_scan_index += 1;
663                    }
664                }
665
666                let intensity_dbl = intensities_.iter().map(|&x| x as f64).collect::<Vec<f64>>();
667                let tof_i32 = tof_indices_.iter().map(|&x| x as i32).collect::<Vec<i32>>();
668
669                let mz = self.index_converter.tof_to_mz(frame_id, &tof_indices_);
670                let inv_mobility = self
671                    .index_converter
672                    .scan_to_inverse_mobility(frame_id, &scan);
673
674                let ms_type_raw = self.raw_data_layout.frame_meta_data[frame_index].ms_ms_type;
675                let ms_type = match ms_type_raw {
676                    0 => MsType::Precursor,
677                    8 => MsType::FragmentDda,
678                    9 => MsType::FragmentDia,
679                    _ => MsType::Unknown,
680                };
681
682                TimsFrame {
683                    frame_id: frame_id as i32,
684                    ms_type,
685                    scan: scan.iter().map(|&x| x as i32).collect(),
686                    tof: tof_i32,
687                    ims_frame: ImsFrame::new(
688                        self.raw_data_layout.frame_meta_data[frame_index].time,
689                        inv_mobility,
690                        mz,
691                        intensity_dbl,
692                    ),
693                }
694            }
695
696            // Existing handling of Type 2
697            2 => {
698                let mut compressed_data = vec![0u8; bin_size as usize - 8];
699                infile.read_exact(&mut compressed_data).unwrap();
700
701                let decompressed_bytes = zstd_decompress(&compressed_data).unwrap();
702
703                let (scan, tof, intensity) =
704                    parse_decompressed_bruker_binary_data(&decompressed_bytes).unwrap();
705                let intensity_dbl = intensity.iter().map(|&x| x as f64).collect();
706                let tof_i32 = tof.iter().map(|&x| x as i32).collect();
707                let scan = flatten_scan_values(&scan, true);
708
709                let mz = self.index_converter.tof_to_mz(frame_id, &tof);
710                let inv_mobility = self
711                    .index_converter
712                    .scan_to_inverse_mobility(frame_id, &scan);
713
714                let ms_type_raw = self.raw_data_layout.frame_meta_data[frame_index].ms_ms_type;
715
716                let ms_type = match ms_type_raw {
717                    0 => MsType::Precursor,
718                    8 => MsType::FragmentDda,
719                    9 => MsType::FragmentDia,
720                    _ => MsType::Unknown,
721                };
722
723                TimsFrame {
724                    frame_id: frame_id as i32,
725                    ms_type,
726                    scan: scan.iter().map(|&x| x as i32).collect(),
727                    tof: tof_i32,
728                    ims_frame: ImsFrame::new(
729                        self.raw_data_layout.frame_meta_data[frame_index].time,
730                        inv_mobility,
731                        mz,
732                        intensity_dbl,
733                    ),
734                }
735            }
736
737            _ => {
738                panic!("TimsCompressionType is not 1 or 2.")
739            }
740        }
741    }
742
743    fn get_raw_frame(&self, frame_id: u32) -> RawTimsFrame {
744        let frame_index = (frame_id - 1) as usize;
745        let offset = self.raw_data_layout.tims_offset_values[frame_index] as u64;
746
747        // turns out, there can be empty frames in the data, check for that, if so, return an empty frame
748        let num_peaks = self.raw_data_layout.frame_meta_data[frame_index].num_peaks;
749
750        if num_peaks == 0 {
751            return RawTimsFrame {
752                frame_id: frame_id as i32,
753                retention_time: self.raw_data_layout.frame_meta_data[(frame_id - 1) as usize].time,
754                ms_type: MsType::Unknown,
755                scan: Vec::new(),
756                tof: Vec::new(),
757                intensity: Vec::new(),
758            };
759        }
760
761        let mut file_path = PathBuf::from(&self.raw_data_layout.raw_data_path);
762        file_path.push("analysis.tdf_bin");
763        let mut infile = File::open(&file_path).unwrap();
764
765        infile.seek(SeekFrom::Start(offset)).unwrap();
766
767        let mut bin_buffer = [0u8; 4];
768        infile.read_exact(&mut bin_buffer).unwrap();
769        let bin_size = Cursor::new(bin_buffer).read_i32::<LittleEndian>().unwrap();
770
771        infile.read_exact(&mut bin_buffer).unwrap();
772
773        match self.raw_data_layout.global_meta_data.tims_compression_type {
774            _ if self.raw_data_layout.global_meta_data.tims_compression_type == 1 => {
775                panic!("Decompression Type1 not implemented.");
776            }
777
778            // Extract from ZSTD compressed binary
779            _ if self.raw_data_layout.global_meta_data.tims_compression_type == 2 => {
780                let mut compressed_data = vec![0u8; bin_size as usize - 8];
781                infile.read_exact(&mut compressed_data).unwrap();
782
783                let decompressed_bytes = zstd_decompress(&compressed_data).unwrap();
784
785                let (scan, tof, intensity) =
786                    parse_decompressed_bruker_binary_data(&decompressed_bytes).unwrap();
787
788                let ms_type_raw = self.raw_data_layout.frame_meta_data[frame_index].ms_ms_type;
789
790                let ms_type = match ms_type_raw {
791                    0 => MsType::Precursor,
792                    8 => MsType::FragmentDda,
793                    9 => MsType::FragmentDia,
794                    _ => MsType::Unknown,
795                };
796
797                let frame = RawTimsFrame {
798                    frame_id: frame_id as i32,
799                    retention_time: self.raw_data_layout.frame_meta_data[(frame_id - 1) as usize]
800                        .time,
801                    ms_type,
802                    scan,
803                    tof,
804                    intensity: intensity.iter().map(|&x| x as f64).collect(),
805                };
806
807                return frame;
808            }
809
810            // Error on unknown compression algorithm
811            _ => {
812                panic!("TimsCompressionType is not 1 or 2.")
813            }
814        }
815    }
816
817    fn get_slice(&self, frame_ids: Vec<u32>, _num_threads: usize) -> TimsSlice {
818        let result: Vec<TimsFrame> = frame_ids.into_iter().map(|f| self.get_frame(f)).collect();
819
820        TimsSlice { frames: result }
821    }
822
823    fn get_acquisition_mode(&self) -> AcquisitionMode {
824        self.raw_data_layout.acquisition_mode.clone()
825    }
826
827    fn get_frame_count(&self) -> i32 {
828        self.raw_data_layout.frame_meta_data.len() as i32
829    }
830
831    fn get_data_path(&self) -> &str {
832        &self.raw_data_layout.raw_data_path
833    }
834}
835
836pub struct TimsInMemoryLoader {
837    pub raw_data_layout: TimsRawDataLayout,
838    pub index_converter: TimsIndexConverter,
839    compressed_data: Vec<u8>,
840}
841
842impl TimsData for TimsInMemoryLoader {
843    fn get_frame(&self, frame_id: u32) -> TimsFrame {
844        let raw_frame = self.get_raw_frame(frame_id);
845
846        let raw_frame = match raw_frame.ms_type {
847            MsType::FragmentDda => raw_frame.smooth(1).centroid(1),
848            _ => raw_frame,
849        };
850
851        // if raw frame is empty, return an empty frame
852        if raw_frame.scan.is_empty() {
853            return TimsFrame::default();
854        }
855
856        let tof_i32 = raw_frame.tof.iter().map(|&x| x as i32).collect();
857        let scan = flatten_scan_values(&raw_frame.scan, true);
858
859        let mz = self.index_converter.tof_to_mz(frame_id, &raw_frame.tof);
860        let inverse_mobility = self
861            .index_converter
862            .scan_to_inverse_mobility(frame_id, &scan);
863
864        let ims_frame = ImsFrame::new(
865            raw_frame.retention_time,
866            inverse_mobility,
867            mz,
868            raw_frame.intensity,
869        );
870
871        TimsFrame {
872            frame_id: frame_id as i32,
873            ms_type: raw_frame.ms_type,
874            scan: scan.iter().map(|&x| x as i32).collect(),
875            tof: tof_i32,
876            ims_frame,
877        }
878    }
879
880    fn get_raw_frame(&self, frame_id: u32) -> RawTimsFrame {
881        let frame_index = (frame_id - 1) as usize;
882        let offset = self.raw_data_layout.tims_offset_values[frame_index] as usize;
883
884        let bin_size_offset = offset + 4; // Assuming the size is stored immediately before the frame data
885        let bin_size = Cursor::new(&self.compressed_data[offset..bin_size_offset])
886            .read_i32::<LittleEndian>()
887            .unwrap();
888
889        let data_offset = bin_size_offset + 4; // Adjust based on actual structure
890        let frame_data = &self.compressed_data[data_offset..data_offset + bin_size as usize - 8];
891
892        let decompressed_bytes = zstd_decompress(&frame_data).unwrap();
893
894        let (scan, tof, intensity) =
895            parse_decompressed_bruker_binary_data(&decompressed_bytes).unwrap();
896
897        let ms_type_raw = self.raw_data_layout.frame_meta_data[frame_index].ms_ms_type;
898
899        let ms_type = match ms_type_raw {
900            0 => MsType::Precursor,
901            8 => MsType::FragmentDda,
902            9 => MsType::FragmentDia,
903            _ => MsType::Unknown,
904        };
905
906        let raw_frame = RawTimsFrame {
907            frame_id: frame_id as i32,
908            retention_time: self.raw_data_layout.frame_meta_data[(frame_id - 1) as usize].time,
909            ms_type,
910            scan,
911            tof,
912            intensity: intensity.iter().map(|&x| x as f64).collect(),
913        };
914
915        raw_frame
916    }
917
918    fn get_slice(&self, frame_ids: Vec<u32>, num_threads: usize) -> TimsSlice {
919        let pool = ThreadPoolBuilder::new()
920            .num_threads(num_threads)
921            .build()
922            .unwrap();
923        let frames = pool.install(|| {
924            frame_ids
925                .par_iter()
926                .map(|&frame_id| self.get_frame(frame_id))
927                .collect()
928        });
929
930        TimsSlice { frames }
931    }
932
933    fn get_acquisition_mode(&self) -> AcquisitionMode {
934        self.raw_data_layout.acquisition_mode.clone()
935    }
936
937    fn get_frame_count(&self) -> i32 {
938        self.raw_data_layout.frame_meta_data.len() as i32
939    }
940
941    fn get_data_path(&self) -> &str {
942        &self.raw_data_layout.raw_data_path
943    }
944}
945
946pub enum TimsDataLoader {
947    InMemory(TimsInMemoryLoader),
948    Lazy(TimsLazyLoder),
949}
950
951/// Pick the m/z/mobility index converter, shared by the lazy and in-memory loaders.
952///
953/// - `use_bruker_sdk` → try the live Bruker SDK converter; if the SDK shared library can't be
954///   loaded (missing / wrong arch / mismatched version) this now **falls back** (with a warning)
955///   instead of panicking.
956/// - Otherwise (or after that fallback): derive an accurate calibration from the SDK if the
957///   `bruker_lib_path` is usable, else the simple boundary model (~5 Da error on some datasets).
958fn build_index_converter(
959    bruker_lib_path: &str,
960    data_path: &str,
961    use_bruker_sdk: bool,
962    scan_max_index: u32,
963    im_lower: f64,
964    im_upper: f64,
965    tof_max_index: u32,
966    mz_lower: f64,
967    mz_upper: f64,
968) -> TimsIndexConverter {
969    if use_bruker_sdk {
970        match BrukerLibTimsDataConverter::try_new(bruker_lib_path, data_path) {
971            Ok(converter) => return TimsIndexConverter::BrukerLib(converter),
972            Err(e) => eprintln!(
973                "Warning: Bruker SDK requested but failed to load ({e}); \
974                falling back to derived/simple m/z calibration."
975            ),
976        }
977    }
978    // Derive an accurate calibration via the SDK if possible, else the simple boundary model.
979    match derive_mz_calibration(bruker_lib_path, data_path, tof_max_index) {
980        Some((intercept, slope)) => TimsIndexConverter::Calibrated(CalibratedIndexConverter::new(
981            intercept,
982            slope,
983            im_lower,
984            im_upper,
985            scan_max_index,
986        )),
987        None => {
988            eprintln!(
989                "Warning: Could not derive m/z calibration from SDK. \
990                Using simple boundary model which may have ~5 Da error on some datasets. \
991                This typically happens on macOS where Bruker SDK is not available."
992            );
993            TimsIndexConverter::Simple(SimpleIndexConverter::from_boundaries(
994                mz_lower,
995                mz_upper,
996                tof_max_index,
997                im_lower,
998                im_upper,
999                scan_max_index,
1000            ))
1001        }
1002    }
1003}
1004
1005impl TimsDataLoader {
1006    pub fn new_lazy(
1007        bruker_lib_path: &str,
1008        data_path: &str,
1009        use_bruker_sdk: bool,
1010        scan_max_index: u32,
1011        im_lower: f64,
1012        im_upper: f64,
1013        tof_max_index: u32,
1014        mz_lower: f64,
1015        mz_upper: f64,
1016    ) -> Self {
1017        let raw_data_layout = TimsRawDataLayout::new(data_path);
1018        let index_converter = build_index_converter(
1019            bruker_lib_path,
1020            data_path,
1021            use_bruker_sdk,
1022            scan_max_index,
1023            im_lower,
1024            im_upper,
1025            tof_max_index,
1026            mz_lower,
1027            mz_upper,
1028        );
1029
1030        TimsDataLoader::Lazy(TimsLazyLoder {
1031            raw_data_layout,
1032            index_converter,
1033        })
1034    }
1035
1036    pub fn new_in_memory(
1037        bruker_lib_path: &str,
1038        data_path: &str,
1039        use_bruker_sdk: bool,
1040        scan_max_index: u32,
1041        im_lower: f64,
1042        im_upper: f64,
1043        tof_max_index: u32,
1044        mz_lower: f64,
1045        mz_upper: f64,
1046    ) -> Self {
1047        let raw_data_layout = TimsRawDataLayout::new(data_path);
1048        let index_converter = build_index_converter(
1049            bruker_lib_path,
1050            data_path,
1051            use_bruker_sdk,
1052            scan_max_index,
1053            im_lower,
1054            im_upper,
1055            tof_max_index,
1056            mz_lower,
1057            mz_upper,
1058        );
1059
1060        let mut file_path = PathBuf::from(data_path);
1061        file_path.push("analysis.tdf_bin");
1062        let mut infile = File::open(file_path).unwrap();
1063        let mut data = Vec::new();
1064        infile.read_to_end(&mut data).unwrap();
1065
1066        TimsDataLoader::InMemory(TimsInMemoryLoader {
1067            raw_data_layout,
1068            index_converter,
1069            compressed_data: data,
1070        })
1071    }
1072
1073    /// Create a lazy loader with pre-computed ion mobility calibration lookup table.
1074    ///
1075    /// This method enables accurate ion mobility calibration with fast parallel extraction.
1076    /// The im_lookup table should be pre-computed using the Bruker SDK.
1077    ///
1078    /// # Arguments
1079    /// * `data_path` - Path to the .d folder
1080    /// * `bruker_lib_path` - Path to the Bruker SDK shared library; used to
1081    ///   derive an accurate m/z calibration. Pass "NO_SDK" (or an empty
1082    ///   string) to skip and use the 2-point boundary m/z model.
1083    /// * `tof_max_index` - Maximum TOF index (from GlobalMetaData)
1084    /// * `mz_lower` - Minimum m/z value (from GlobalMetaData)
1085    /// * `mz_upper` - Maximum m/z value (from GlobalMetaData)
1086    /// * `im_lookup` - Pre-computed scan→1/K0 lookup table
1087    ///
1088    /// # Returns
1089    /// A new TimsDataLoader with LookupIndexConverter
1090    pub fn new_lazy_with_calibration(
1091        data_path: &str,
1092        bruker_lib_path: &str,
1093        tof_max_index: u32,
1094        mz_lower: f64,
1095        mz_upper: f64,
1096        im_lookup: Vec<f64>,
1097    ) -> Self {
1098        let raw_data_layout = TimsRawDataLayout::new(data_path);
1099
1100        let index_converter = TimsIndexConverter::Lookup(build_lookup_converter(
1101            bruker_lib_path,
1102            data_path,
1103            tof_max_index,
1104            mz_lower,
1105            mz_upper,
1106            im_lookup,
1107        ));
1108
1109        TimsDataLoader::Lazy(TimsLazyLoder {
1110            raw_data_layout,
1111            index_converter,
1112        })
1113    }
1114
1115    /// Create an in-memory loader with pre-computed ion mobility calibration lookup table.
1116    ///
1117    /// This method enables accurate ion mobility calibration with fast parallel extraction.
1118    /// The im_lookup table should be pre-computed using the Bruker SDK.
1119    ///
1120    /// # Arguments
1121    /// * `data_path` - Path to the .d folder
1122    /// * `bruker_lib_path` - Path to the Bruker SDK shared library; used to
1123    ///   derive an accurate m/z calibration. Pass "NO_SDK" (or an empty
1124    ///   string) to skip and use the 2-point boundary m/z model.
1125    /// * `tof_max_index` - Maximum TOF index (from GlobalMetaData)
1126    /// * `mz_lower` - Minimum m/z value (from GlobalMetaData)
1127    /// * `mz_upper` - Maximum m/z value (from GlobalMetaData)
1128    /// * `im_lookup` - Pre-computed scan→1/K0 lookup table
1129    ///
1130    /// # Returns
1131    /// A new TimsDataLoader with LookupIndexConverter
1132    pub fn new_in_memory_with_calibration(
1133        data_path: &str,
1134        bruker_lib_path: &str,
1135        tof_max_index: u32,
1136        mz_lower: f64,
1137        mz_upper: f64,
1138        im_lookup: Vec<f64>,
1139    ) -> Self {
1140        let raw_data_layout = TimsRawDataLayout::new(data_path);
1141
1142        let index_converter = TimsIndexConverter::Lookup(build_lookup_converter(
1143            bruker_lib_path,
1144            data_path,
1145            tof_max_index,
1146            mz_lower,
1147            mz_upper,
1148            im_lookup,
1149        ));
1150
1151        let mut file_path = PathBuf::from(data_path);
1152        file_path.push("analysis.tdf_bin");
1153        let mut infile = File::open(file_path).unwrap();
1154        let mut data = Vec::new();
1155        infile.read_to_end(&mut data).unwrap();
1156
1157        TimsDataLoader::InMemory(TimsInMemoryLoader {
1158            raw_data_layout,
1159            index_converter,
1160            compressed_data: data,
1161        })
1162    }
1163
1164    /// Create a lazy loader using the exact SDK-free Bruker calibration formulas.
1165    ///
1166    /// Builds a [`BrukerFormulaConverter`] from the `MzCalibration` /
1167    /// `TimsCalibration` tables (frame `calibration_frame_id`, default 1 — the
1168    /// coefficients are near-constant per run). Needs no Bruker SDK at build or
1169    /// runtime; 1/K0 is machine-exact and m/z is bit-exact for MzCalibration
1170    /// ModelType 1 (few ppm for ModelType 2).
1171    pub fn new_lazy_with_bruker_formula(data_path: &str, calibration_frame_id: u32) -> Self {
1172        let raw_data_layout = TimsRawDataLayout::new(data_path);
1173        let index_converter = TimsIndexConverter::BrukerFormula(
1174            BrukerFormulaConverter::from_d_folder(data_path, calibration_frame_id).unwrap(),
1175        );
1176        TimsDataLoader::Lazy(TimsLazyLoder {
1177            raw_data_layout,
1178            index_converter,
1179        })
1180    }
1181
1182    /// In-memory counterpart of [`Self::new_lazy_with_bruker_formula`].
1183    pub fn new_in_memory_with_bruker_formula(data_path: &str, calibration_frame_id: u32) -> Self {
1184        let raw_data_layout = TimsRawDataLayout::new(data_path);
1185        let index_converter = TimsIndexConverter::BrukerFormula(
1186            BrukerFormulaConverter::from_d_folder(data_path, calibration_frame_id).unwrap(),
1187        );
1188
1189        let mut file_path = PathBuf::from(data_path);
1190        file_path.push("analysis.tdf_bin");
1191        let mut infile = File::open(file_path).unwrap();
1192        let mut data = Vec::new();
1193        infile.read_to_end(&mut data).unwrap();
1194
1195        TimsDataLoader::InMemory(TimsInMemoryLoader {
1196            raw_data_layout,
1197            index_converter,
1198            compressed_data: data,
1199        })
1200    }
1201
1202    /// Create a lazy loader with full calibration (both m/z and IM).
1203    ///
1204    /// This method uses regression-derived m/z calibration coefficients instead of
1205    /// the simple boundary model, providing more accurate m/z conversion.
1206    ///
1207    /// # Arguments
1208    /// * `data_path` - Path to the .d folder
1209    /// * `tof_intercept` - Intercept for sqrt(mz) = intercept + slope * tof
1210    /// * `tof_slope` - Slope for sqrt(mz) = intercept + slope * tof
1211    /// * `im_min` - Minimum 1/K0 value
1212    /// * `im_max` - Maximum 1/K0 value
1213    /// * `scan_max_index` - Maximum scan index
1214    pub fn new_lazy_with_mz_calibration(
1215        data_path: &str,
1216        tof_intercept: f64,
1217        tof_slope: f64,
1218        im_min: f64,
1219        im_max: f64,
1220        scan_max_index: u32,
1221    ) -> Self {
1222        let raw_data_layout = TimsRawDataLayout::new(data_path);
1223
1224        let index_converter = TimsIndexConverter::Calibrated(CalibratedIndexConverter::new(
1225            tof_intercept,
1226            tof_slope,
1227            im_min,
1228            im_max,
1229            scan_max_index,
1230        ));
1231
1232        TimsDataLoader::Lazy(TimsLazyLoder {
1233            raw_data_layout,
1234            index_converter,
1235        })
1236    }
1237
1238    /// Create an in-memory loader with full calibration (both m/z and IM).
1239    ///
1240    /// This method uses regression-derived m/z calibration coefficients instead of
1241    /// the simple boundary model, providing more accurate m/z conversion.
1242    pub fn new_in_memory_with_mz_calibration(
1243        data_path: &str,
1244        tof_intercept: f64,
1245        tof_slope: f64,
1246        im_min: f64,
1247        im_max: f64,
1248        scan_max_index: u32,
1249    ) -> Self {
1250        let raw_data_layout = TimsRawDataLayout::new(data_path);
1251
1252        let index_converter = TimsIndexConverter::Calibrated(CalibratedIndexConverter::new(
1253            tof_intercept,
1254            tof_slope,
1255            im_min,
1256            im_max,
1257            scan_max_index,
1258        ));
1259
1260        let mut file_path = PathBuf::from(data_path);
1261        file_path.push("analysis.tdf_bin");
1262        let mut infile = File::open(file_path).unwrap();
1263        let mut data = Vec::new();
1264        infile.read_to_end(&mut data).unwrap();
1265
1266        TimsDataLoader::InMemory(TimsInMemoryLoader {
1267            raw_data_layout,
1268            index_converter,
1269            compressed_data: data,
1270        })
1271    }
1272
1273    pub fn get_index_converter(&self) -> &dyn IndexConverter {
1274        match self {
1275            TimsDataLoader::InMemory(loader) => &loader.index_converter,
1276            TimsDataLoader::Lazy(loader) => &loader.index_converter,
1277        }
1278    }
1279
1280    /// Check if the Bruker SDK is being used for index conversion.
1281    /// The Bruker SDK is NOT thread-safe, so parallel operations that call
1282    /// the index converter must be disabled when using the SDK.
1283    pub fn uses_bruker_sdk(&self) -> bool {
1284        match self {
1285            TimsDataLoader::InMemory(loader) => matches!(&loader.index_converter, TimsIndexConverter::BrukerLib(_)),
1286            TimsDataLoader::Lazy(loader) => matches!(&loader.index_converter, TimsIndexConverter::BrukerLib(_)),
1287        }
1288    }
1289}
1290
1291impl TimsData for TimsDataLoader {
1292    fn get_frame(&self, frame_id: u32) -> TimsFrame {
1293        match self {
1294            TimsDataLoader::InMemory(loader) => loader.get_frame(frame_id),
1295            TimsDataLoader::Lazy(loader) => loader.get_frame(frame_id),
1296        }
1297    }
1298    fn get_raw_frame(&self, frame_id: u32) -> RawTimsFrame {
1299        match self {
1300            TimsDataLoader::InMemory(loader) => loader.get_raw_frame(frame_id),
1301            TimsDataLoader::Lazy(loader) => loader.get_raw_frame(frame_id),
1302        }
1303    }
1304
1305    fn get_slice(&self, frame_ids: Vec<u32>, num_threads: usize) -> TimsSlice {
1306        match self {
1307            TimsDataLoader::InMemory(loader) => loader.get_slice(frame_ids, num_threads),
1308            TimsDataLoader::Lazy(loader) => loader.get_slice(frame_ids, num_threads),
1309        }
1310    }
1311
1312    fn get_acquisition_mode(&self) -> AcquisitionMode {
1313        match self {
1314            TimsDataLoader::InMemory(loader) => loader.get_acquisition_mode(),
1315            TimsDataLoader::Lazy(loader) => loader.get_acquisition_mode(),
1316        }
1317    }
1318
1319    fn get_frame_count(&self) -> i32 {
1320        match self {
1321            TimsDataLoader::InMemory(loader) => loader.get_frame_count(),
1322            TimsDataLoader::Lazy(loader) => loader.get_frame_count(),
1323        }
1324    }
1325
1326    fn get_data_path(&self) -> &str {
1327        match self {
1328            TimsDataLoader::InMemory(loader) => loader.get_data_path(),
1329            TimsDataLoader::Lazy(loader) => loader.get_data_path(),
1330        }
1331    }
1332}
1333
1334pub struct SimpleIndexConverter {
1335    pub tof_intercept: f64,
1336    pub tof_slope: f64,
1337    pub scan_intercept: f64,
1338    pub scan_slope: f64,
1339}
1340
1341impl SimpleIndexConverter {
1342    pub fn from_boundaries(
1343        mz_min: f64,
1344        mz_max: f64,
1345        tof_max_index: u32,
1346        im_min: f64,
1347        im_max: f64,
1348        scan_max_index: u32,
1349    ) -> Self {
1350        let tof_intercept: f64 = mz_min.sqrt();
1351        let tof_slope: f64 = (mz_max.sqrt() - tof_intercept) / tof_max_index as f64;
1352
1353        let scan_intercept: f64 = im_max;
1354        let scan_slope: f64 = (im_min - scan_intercept) / scan_max_index as f64;
1355        Self {
1356            tof_intercept,
1357            tof_slope,
1358            scan_intercept,
1359            scan_slope,
1360        }
1361    }
1362}
1363
1364impl IndexConverter for SimpleIndexConverter {
1365    fn tof_to_mz(&self, _frame_id: u32, _tof_values: &Vec<u32>) -> Vec<f64> {
1366        let mut mz_values: Vec<f64> = Vec::new();
1367        mz_values.resize(_tof_values.len(), 0.0);
1368
1369        for (i, &val) in _tof_values.iter().enumerate() {
1370            mz_values[i] = (self.tof_intercept + self.tof_slope * val as f64).powi(2);
1371        }
1372
1373        mz_values
1374    }
1375
1376    fn mz_to_tof(&self, _frame_id: u32, _mz_values: &Vec<f64>) -> Vec<u32> {
1377        let mut tof_values: Vec<u32> = Vec::new();
1378        tof_values.resize(_mz_values.len(), 0);
1379
1380        for (i, &val) in _mz_values.iter().enumerate() {
1381            tof_values[i] = ((val.sqrt() - self.tof_intercept) / self.tof_slope) as u32;
1382        }
1383
1384        tof_values
1385    }
1386
1387    fn scan_to_inverse_mobility(&self, _frame_id: u32, _scan_values: &Vec<u32>) -> Vec<f64> {
1388        let mut inv_mobility_values: Vec<f64> = Vec::new();
1389        inv_mobility_values.resize(_scan_values.len(), 0.0);
1390
1391        for (i, &val) in _scan_values.iter().enumerate() {
1392            inv_mobility_values[i] = self.scan_intercept + self.scan_slope * val as f64;
1393        }
1394
1395        inv_mobility_values
1396    }
1397
1398    fn inverse_mobility_to_scan(
1399        &self,
1400        _frame_id: u32,
1401        _inverse_mobility_values: &Vec<f64>,
1402    ) -> Vec<u32> {
1403        let mut scan_values: Vec<u32> = Vec::new();
1404        scan_values.resize(_inverse_mobility_values.len(), 0);
1405
1406        for (i, &val) in _inverse_mobility_values.iter().enumerate() {
1407            scan_values[i] = ((val - self.scan_intercept) / self.scan_slope) as u32;
1408        }
1409
1410        scan_values
1411    }
1412}
1413
1414/// M/z calibrated index converter using regression-derived coefficients.
1415///
1416/// This provides accurate TOF to m/z conversion without requiring the Bruker SDK
1417/// by using linear regression coefficients derived from known precursor m/z values.
1418///
1419/// The calibration formula is:
1420///   sqrt(mz) = tof_intercept + tof_slope * tof_index
1421///
1422/// This is similar to SimpleIndexConverter but uses externally-provided coefficients
1423/// (e.g., from regression on precursor data) rather than boundary-derived values.
1424pub struct CalibratedIndexConverter {
1425    pub tof_intercept: f64,
1426    pub tof_slope: f64,
1427    pub scan_intercept: f64,
1428    pub scan_slope: f64,
1429}
1430
1431impl CalibratedIndexConverter {
1432    /// Create a new calibrated converter with regression-derived coefficients.
1433    ///
1434    /// # Arguments
1435    /// * `tof_intercept` - Intercept for sqrt(mz) = intercept + slope * tof
1436    /// * `tof_slope` - Slope for sqrt(mz) = intercept + slope * tof
1437    /// * `im_min` - Minimum 1/K0 value
1438    /// * `im_max` - Maximum 1/K0 value
1439    /// * `scan_max_index` - Maximum scan index
1440    pub fn new(
1441        tof_intercept: f64,
1442        tof_slope: f64,
1443        im_min: f64,
1444        im_max: f64,
1445        scan_max_index: u32,
1446    ) -> Self {
1447        let scan_intercept = im_max;
1448        let scan_slope = (im_min - scan_intercept) / scan_max_index as f64;
1449        Self {
1450            tof_intercept,
1451            tof_slope,
1452            scan_intercept,
1453            scan_slope,
1454        }
1455    }
1456}
1457
1458impl IndexConverter for CalibratedIndexConverter {
1459    fn tof_to_mz(&self, _frame_id: u32, tof_values: &Vec<u32>) -> Vec<f64> {
1460        let mut mz_values: Vec<f64> = Vec::with_capacity(tof_values.len());
1461
1462        for &tof_index in tof_values.iter() {
1463            // sqrt(mz) = tof_intercept + tof_slope * tof_index
1464            let sqrt_mz = self.tof_intercept + self.tof_slope * tof_index as f64;
1465            mz_values.push(sqrt_mz * sqrt_mz);
1466        }
1467
1468        mz_values
1469    }
1470
1471    fn mz_to_tof(&self, _frame_id: u32, mz_values: &Vec<f64>) -> Vec<u32> {
1472        let mut tof_values: Vec<u32> = Vec::with_capacity(mz_values.len());
1473
1474        for &mz in mz_values.iter() {
1475            let sqrt_mz = mz.sqrt();
1476            // tof_index = (sqrt(mz) - tof_intercept) / tof_slope
1477            tof_values.push(((sqrt_mz - self.tof_intercept) / self.tof_slope) as u32);
1478        }
1479
1480        tof_values
1481    }
1482
1483    fn scan_to_inverse_mobility(&self, _frame_id: u32, scan_values: &Vec<u32>) -> Vec<f64> {
1484        let mut inv_mobility_values: Vec<f64> = Vec::with_capacity(scan_values.len());
1485
1486        for &val in scan_values.iter() {
1487            inv_mobility_values.push(self.scan_intercept + self.scan_slope * val as f64);
1488        }
1489
1490        inv_mobility_values
1491    }
1492
1493    fn inverse_mobility_to_scan(
1494        &self,
1495        _frame_id: u32,
1496        inverse_mobility_values: &Vec<f64>,
1497    ) -> Vec<u32> {
1498        let mut scan_values: Vec<u32> = Vec::with_capacity(inverse_mobility_values.len());
1499
1500        for &val in inverse_mobility_values.iter() {
1501            scan_values.push(((val - self.scan_intercept) / self.scan_slope) as u32);
1502        }
1503
1504        scan_values
1505    }
1506}
1507
1508/// Ion mobility index converter using pre-computed lookup table.
1509///
1510/// This converter uses a pre-computed scan→1/K0 lookup table extracted from the Bruker SDK.
1511/// It enables accurate ion mobility calibration with fast parallel extraction.
1512///
1513/// Background:
1514/// - The Bruker calibration formula is patented and proprietary
1515/// - Using the Bruker SDK gives accurate values but is slow (not thread-safe)
1516/// - Linear interpolation is fast but inaccurate
1517/// - This converter uses SDK-probed lookup for accuracy with O(1) thread-safe lookups
1518///
1519/// The lookup table is typically small (~8KB for 1000 scans) and constant across all frames.
1520pub struct LookupIndexConverter {
1521    // m/z conversion uses simple linear model (accurate enough for most purposes)
1522    pub tof_intercept: f64,
1523    pub tof_slope: f64,
1524
1525    // Ion mobility: pre-computed lookup table from Bruker SDK
1526    // scan_index → 1/K0 value
1527    pub im_lookup: Vec<f64>,
1528
1529    // Fallback for inverse conversion (1/K0 → scan)
1530    // We store the min/max for binary search bounds
1531    pub im_min: f64,
1532    pub im_max: f64,
1533}
1534
1535impl LookupIndexConverter {
1536    /// Create a new LookupIndexConverter with pre-computed ion mobility lookup.
1537    ///
1538    /// # Arguments
1539    /// * `mz_min` - Minimum m/z value for TOF conversion
1540    /// * `mz_max` - Maximum m/z value for TOF conversion
1541    /// * `tof_max_index` - Maximum TOF index
1542    /// * `im_lookup` - Pre-computed scan→1/K0 lookup table from Bruker SDK
1543    ///
1544    /// # Returns
1545    /// A new LookupIndexConverter instance
1546    pub fn new(
1547        mz_min: f64,
1548        mz_max: f64,
1549        tof_max_index: u32,
1550        im_lookup: Vec<f64>,
1551    ) -> Self {
1552        let tof_intercept: f64 = mz_min.sqrt();
1553        let tof_slope: f64 = (mz_max.sqrt() - tof_intercept) / tof_max_index as f64;
1554
1555        // Get IM bounds for inverse conversion
1556        let im_min = im_lookup.iter().cloned().fold(f64::INFINITY, f64::min);
1557        let im_max = im_lookup.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1558
1559        Self {
1560            tof_intercept,
1561            tof_slope,
1562            im_lookup,
1563            im_min,
1564            im_max,
1565        }
1566    }
1567
1568    /// Create a `LookupIndexConverter` with regression-derived m/z coefficients.
1569    ///
1570    /// Uses `sqrt(mz) = tof_intercept + tof_slope * tof` with coefficients fitted
1571    /// from the Bruker SDK (see `derive_mz_calibration`), instead of the 2-point
1572    /// boundary model in `new`. Preferred whenever the SDK is available.
1573    ///
1574    /// # Arguments
1575    /// * `tof_intercept` - Intercept of the sqrt(mz)-vs-tof regression
1576    /// * `tof_slope` - Slope of the sqrt(mz)-vs-tof regression
1577    /// * `im_lookup` - Pre-computed scan→1/K0 lookup table from Bruker SDK
1578    pub fn with_mz_fit(tof_intercept: f64, tof_slope: f64, im_lookup: Vec<f64>) -> Self {
1579        // Get IM bounds for inverse conversion
1580        let im_min = im_lookup.iter().cloned().fold(f64::INFINITY, f64::min);
1581        let im_max = im_lookup.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1582
1583        Self {
1584            tof_intercept,
1585            tof_slope,
1586            im_lookup,
1587            im_min,
1588            im_max,
1589        }
1590    }
1591}
1592
1593impl IndexConverter for LookupIndexConverter {
1594    fn tof_to_mz(&self, _frame_id: u32, tof_values: &Vec<u32>) -> Vec<f64> {
1595        let mut mz_values: Vec<f64> = Vec::new();
1596        mz_values.resize(tof_values.len(), 0.0);
1597
1598        for (i, &val) in tof_values.iter().enumerate() {
1599            mz_values[i] = (self.tof_intercept + self.tof_slope * val as f64).powi(2);
1600        }
1601
1602        mz_values
1603    }
1604
1605    fn mz_to_tof(&self, _frame_id: u32, mz_values: &Vec<f64>) -> Vec<u32> {
1606        let mut tof_values: Vec<u32> = Vec::new();
1607        tof_values.resize(mz_values.len(), 0);
1608
1609        for (i, &val) in mz_values.iter().enumerate() {
1610            tof_values[i] = ((val.sqrt() - self.tof_intercept) / self.tof_slope) as u32;
1611        }
1612
1613        tof_values
1614    }
1615
1616    fn scan_to_inverse_mobility(&self, _frame_id: u32, scan_values: &Vec<u32>) -> Vec<f64> {
1617        // Use the pre-computed lookup table for O(1) conversion
1618        scan_values
1619            .iter()
1620            .map(|&s| {
1621                self.im_lookup
1622                    .get(s as usize)
1623                    .copied()
1624                    .unwrap_or(f64::NAN)
1625            })
1626            .collect()
1627    }
1628
1629    fn inverse_mobility_to_scan(
1630        &self,
1631        _frame_id: u32,
1632        inverse_mobility_values: &Vec<f64>,
1633    ) -> Vec<u32> {
1634        // Use binary search to find the closest scan index for each 1/K0 value
1635        // The lookup table is monotonically decreasing (higher scan = lower 1/K0)
1636        inverse_mobility_values
1637            .iter()
1638            .map(|&im| {
1639                if im.is_nan() || self.im_lookup.is_empty() {
1640                    return 0;
1641                }
1642
1643                // Binary search for the closest value
1644                // Note: im_lookup is typically monotonically decreasing
1645                let mut best_scan = 0usize;
1646                let mut best_diff = f64::INFINITY;
1647
1648                for (scan, &lookup_im) in self.im_lookup.iter().enumerate() {
1649                    let diff = (lookup_im - im).abs();
1650                    if diff < best_diff {
1651                        best_diff = diff;
1652                        best_scan = scan;
1653                    }
1654                }
1655
1656                best_scan as u32
1657            })
1658            .collect()
1659    }
1660}