rustdf/data/raw.rs
1use libloading::{Library, Symbol};
2use std::cell::RefCell;
3use std::os::raw::{c_char, c_double, c_float};
4use std::sync::Mutex;
5
6//
7// A struct that holds a handle to the raw data
8//
9// # Example
10//
11// ```
12// let bruker_lib_path = "path/to/libtimsdata.so";
13// let data_path = "path/to/data.d";
14// let tims_data = BrukerTimsDataLibrary::new(bruker_lib_path, data_path);
15// ```
16pub struct BrukerTimsDataLibrary {
17 pub lib: Library,
18 pub handle: u64,
19}
20
21impl BrukerTimsDataLibrary {
22 //
23 // Create a new BrukerTimsDataLibrary struct
24 //
25 // # Arguments
26 //
27 // * `bruker_lib_path` - A string slice that holds the path to the bruker library
28 // * `data_path` - A string slice that holds the path to the data
29 //
30 // # Example
31 //
32 // ```
33 // let bruker_lib_path = "path/to/libtimsdata.so";
34 // let data_path = "path/to/data.d";
35 // let tims_data = BrukerTimsDataLibrary::new(bruker_lib_path, data_path);
36 // ```
37 pub fn new(
38 bruker_lib_path: &str,
39 data_path: &str,
40 ) -> Result<BrukerTimsDataLibrary, Box<dyn std::error::Error>> {
41 // Load the library
42 let lib = unsafe { Library::new(bruker_lib_path)? };
43
44 // create a handle to the raw data
45 let handle = unsafe {
46 let func: Symbol<unsafe extern "C" fn(*const c_char, u32) -> u64> =
47 lib.get(b"tims_open")?;
48 let path = std::ffi::CString::new(data_path)?;
49 let handle = func(path.as_ptr(), 0);
50 handle
51 };
52
53 // return the BrukerTimsDataLibrary struct
54 Ok(BrukerTimsDataLibrary { lib, handle })
55 }
56
57 //
58 // Close the handle to the raw data
59 //
60 // # Example
61 //
62 // ```
63 // let close = tims_data.tims_close();
64 // match close {
65 // Ok(_) => println!("tims_data closed"),
66 // Err(e) => println!("error: {}", e),
67 // };
68 // ```
69 pub fn tims_close(&self) -> Result<(), Box<dyn std::error::Error>> {
70 unsafe {
71 let func: Symbol<unsafe extern "C" fn(u64) -> ()> = self.lib.get(b"tims_close")?;
72 func(self.handle);
73 }
74 Ok(())
75 }
76
77 //
78 // Convert the given indices to mz values.
79 //
80 // # Example
81 //
82 // ```
83 // let indices = vec![...];
84 // let mz_values_result = tims_data.tims_index_to_mz(estimation, &mut indices, tof_max_index);
85 // match mz_values_result {
86 // Ok(mz_values) => println!("{:?}", mz_values),
87 // Err(e) => println!("error: {}", e),
88 // };
89 // ```
90 pub fn tims_index_to_mz(
91 &self,
92 frame_id: u32,
93 dbl_tofs: &[c_double],
94 mzs: &mut [c_double],
95 ) -> Result<(), Box<dyn std::error::Error>> {
96 unsafe {
97 let func: Symbol<unsafe extern "C" fn(u64, u32, *const c_double, *mut c_double, u32)> =
98 self.lib.get(b"tims_index_to_mz")?;
99 func(
100 self.handle,
101 frame_id,
102 dbl_tofs.as_ptr(),
103 mzs.as_mut_ptr(),
104 dbl_tofs.len() as u32,
105 );
106 }
107 Ok(())
108 }
109
110 //
111 // Convert the given mz values to indices.
112 //
113 // # Example
114 //
115 // ```
116 // let mzs = vec![...];
117 // let indices_result = tims_data.tims_mz_to_index(estimation, &mut mzs);
118 // match indices_result {
119 // Ok(indices) => println!("{:?}", indices),
120 // Err(e) => println!("error: {}", e),
121 // };
122 // ```
123 pub fn tims_mz_to_index(
124 &self,
125 frame_id: u32,
126 mzs: &[c_double],
127 indices: &mut [c_double],
128 ) -> Result<(), Box<dyn std::error::Error>> {
129 unsafe {
130 let func: Symbol<unsafe extern "C" fn(u64, u32, *const c_double, *mut c_double, u32)> =
131 self.lib.get(b"tims_mz_to_index")?;
132 func(
133 self.handle,
134 frame_id,
135 mzs.as_ptr(),
136 indices.as_mut_ptr(),
137 mzs.len() as u32,
138 );
139 }
140 Ok(())
141 }
142
143 //
144 // Convert the given indices to inverse mobility values.
145 //
146 // # Example
147 //
148 // ```
149 // let indices = vec![...];
150 // let scan_values_result = tims_data.tims_scan_to_inv_mob(estimation, &mut indices);
151 // match mz_values_result {
152 // Ok(mz_values) => println!("{:?}", mz_values),
153 // Err(e) => println!("error: {}", e),
154 // };
155 // ```
156 pub fn tims_scan_to_inv_mob(
157 &self,
158 frame_id: u32,
159 dbl_scans: &[c_double],
160 inv_mob: &mut [c_double],
161 ) -> Result<(), Box<dyn std::error::Error>> {
162 unsafe {
163 let func: Symbol<unsafe extern "C" fn(u64, u32, *const c_double, *mut c_double, u32)> =
164 self.lib.get(b"tims_scannum_to_oneoverk0")?;
165 func(
166 self.handle,
167 frame_id,
168 dbl_scans.as_ptr(),
169 inv_mob.as_mut_ptr(),
170 dbl_scans.len() as u32,
171 );
172 }
173 Ok(())
174 }
175
176 //
177 // Convert the given inverse mobility values to scan values.
178 //
179 // # Example
180 //
181 // ```
182 // let inv_mob = vec![...];
183 // let scan_values_result = tims_data.tims_inv_mob_to_scan(estimation, &mut inv_mob);
184 // match mz_values_result {
185 // Ok(mz_values) => println!("{:?}", mz_values),
186 // Err(e) => println!("error: {}", e),
187 // };
188 // ```
189 pub fn inv_mob_to_tims_scan(
190 &self,
191 frame_id: u32,
192 inv_mob: &[c_double],
193 scans: &mut [c_double],
194 ) -> Result<(), Box<dyn std::error::Error>> {
195 unsafe {
196 let func: Symbol<unsafe extern "C" fn(u64, u32, *const c_double, *mut c_double, u32)> =
197 self.lib.get(b"tims_oneoverk0_to_scannum")?;
198 func(
199 self.handle,
200 frame_id,
201 inv_mob.as_ptr(),
202 scans.as_mut_ptr(),
203 inv_mob.len() as u32,
204 );
205 }
206 Ok(())
207 }
208
209 // ----------------------------------------------------------------- //
210 // Centroided spectrum extraction (Bruker's built-in peak picker).
211 //
212 // SDK function:
213 // uint32_t tims_extract_centroided_spectrum_for_frame_v2(
214 // uint64_t handle,
215 // int64_t frame_id,
216 // uint32_t scan_begin,
217 // uint32_t scan_end,
218 // void (*callback)(int64_t precursor_id, uint32_t num_peaks,
219 // double* mzs, float* intensities),
220 // void* user_data);
221 //
222 // Bruker invokes the callback ONCE per scan-range with the centroided
223 // (m/z, intensity) arrays. We use a thread-local Mutex<Vec<...>> trick
224 // to hand the data back to Rust without dealing with `void*` user_data
225 // closures (libloading + C function pointers don't compose with Rust
226 // closures cleanly).
227 //
228 // Signatures recovered from pyTDFSDK (gtluu/pyTDFSDK init_tdf_sdk.py).
229 // Same callback shape as Bruker's published `MSMS_SPECTRUM_FUNCTOR`.
230 // ----------------------------------------------------------------- //
231
232 /// Extract a centroided spectrum for a (frame, scan-range) tile via
233 /// Bruker's built-in peak picker. Returns `(mz, intensity)` pairs.
234 pub fn tims_extract_centroided_spectrum_for_frame(
235 &self,
236 frame_id: i64,
237 scan_begin: u32,
238 scan_end: u32,
239 ) -> Result<(Vec<f64>, Vec<f32>), Box<dyn std::error::Error>> {
240 // Stash the result in a thread-local-ish global; only one extract
241 // call may run at a time per process. We serialise on a Mutex.
242 let mut result_mz: Vec<f64> = Vec::new();
243 let mut result_int: Vec<f32> = Vec::new();
244 // We trampoline through a global: the C callback writes into
245 // EXTRACT_BUF.
246 let _guard = EXTRACT_BUF
247 .lock()
248 .map_err(|_| "EXTRACT_BUF poisoned")?;
249 EXTRACT_BUF_DATA.with(|buf| *buf.borrow_mut() = Some((Vec::new(), Vec::new())));
250 unsafe {
251 let func: Symbol<
252 unsafe extern "C" fn(
253 u64, i64, u32, u32,
254 extern "C" fn(i64, u32, *const f64, *const f32),
255 *mut std::ffi::c_void,
256 ) -> u32,
257 > = self.lib.get(b"tims_extract_centroided_spectrum_for_frame_v2")?;
258 let rc = func(
259 self.handle,
260 frame_id,
261 scan_begin,
262 scan_end,
263 centroid_trampoline,
264 std::ptr::null_mut(),
265 );
266 if rc == 0 {
267 return Err("tims_extract_centroided_spectrum_for_frame_v2 returned 0".into());
268 }
269 }
270 if let Some((mz, intens)) = EXTRACT_BUF_DATA.with(|buf| buf.borrow_mut().take()) {
271 result_mz = mz;
272 result_int = intens;
273 }
274 Ok((result_mz, result_int))
275 }
276
277 /// PASEF MS/MS centroided peaks for one MS2 frame.
278 /// SDK: tims_read_pasef_msms_for_frame_v2(handle, frame_id, callback, void**)
279 /// Bruker invokes the callback ONCE PER PRECURSOR found in the frame
280 /// (DDA-PASEF). We accumulate all (precursor_id, mz, intensity) hits.
281 pub fn tims_read_pasef_msms_for_frame(
282 &self,
283 frame_id: i64,
284 ) -> Result<Vec<(i64, Vec<f64>, Vec<f32>)>, Box<dyn std::error::Error>> {
285 let _guard = PASEF_BUF.lock().map_err(|_| "PASEF_BUF poisoned")?;
286 PASEF_BUF_DATA.with(|buf| *buf.borrow_mut() = Some(Vec::new()));
287 unsafe {
288 let func: Symbol<
289 unsafe extern "C" fn(
290 u64, i64,
291 extern "C" fn(i64, u32, *const f64, *const f32, *mut *mut std::ffi::c_void),
292 *mut *mut std::ffi::c_void,
293 ) -> u32,
294 > = self.lib.get(b"tims_read_pasef_msms_for_frame_v2")?;
295 let rc = func(
296 self.handle,
297 frame_id,
298 pasef_trampoline,
299 std::ptr::null_mut(),
300 );
301 if rc == 0 {
302 return Err("tims_read_pasef_msms_for_frame_v2 returned 0".into());
303 }
304 }
305 let out = PASEF_BUF_DATA.with(|buf| buf.borrow_mut().take()).unwrap_or_default();
306 Ok(out)
307 }
308}
309
310// Globals for the C-callback trampolines. Locked on each extract call so
311// only one thread runs the SDK at a time per process — same restriction
312// pyTDFSDK + alphatims live with.
313static EXTRACT_BUF: Mutex<()> = Mutex::new(());
314thread_local! {
315 static EXTRACT_BUF_DATA: RefCell<Option<(Vec<f64>, Vec<f32>)>> = const { RefCell::new(None) };
316}
317
318extern "C" fn centroid_trampoline(
319 _precursor_id: i64,
320 n_peaks: u32,
321 mzs: *const f64,
322 intensities: *const f32,
323) {
324 if n_peaks == 0 || mzs.is_null() || intensities.is_null() { return; }
325 let mz_slice = unsafe { std::slice::from_raw_parts(mzs, n_peaks as usize) };
326 let in_slice = unsafe { std::slice::from_raw_parts(intensities, n_peaks as usize) };
327 EXTRACT_BUF_DATA.with(|buf| {
328 if let Some((ref mut mz_acc, ref mut in_acc)) = *buf.borrow_mut() {
329 mz_acc.extend_from_slice(mz_slice);
330 in_acc.extend_from_slice(in_slice);
331 }
332 });
333}
334
335static PASEF_BUF: Mutex<()> = Mutex::new(());
336thread_local! {
337 static PASEF_BUF_DATA: RefCell<Option<Vec<(i64, Vec<f64>, Vec<f32>)>>> = const { RefCell::new(None) };
338}
339
340extern "C" fn pasef_trampoline(
341 precursor_id: i64,
342 n_peaks: u32,
343 mzs: *const f64,
344 intensities: *const f32,
345 _user_data: *mut *mut std::ffi::c_void,
346) {
347 if n_peaks == 0 || mzs.is_null() || intensities.is_null() { return; }
348 let mz_slice = unsafe { std::slice::from_raw_parts(mzs, n_peaks as usize) };
349 let in_slice = unsafe { std::slice::from_raw_parts(intensities, n_peaks as usize) };
350 PASEF_BUF_DATA.with(|buf| {
351 if let Some(ref mut acc) = *buf.borrow_mut() {
352 acc.push((precursor_id, mz_slice.to_vec(), in_slice.to_vec()));
353 }
354 });
355}
356
357// Silence the unused `c_float` warning on platforms where it's only
358// touched by callbacks above.
359#[allow(dead_code)]
360const _: fn() = || { let _: c_float = 0.0; };
361
362impl Drop for BrukerTimsDataLibrary {
363 fn drop(&mut self) {
364 let close = self.tims_close();
365 match close {
366 Ok(_) => (),
367 Err(e) => println!("error: {}", e),
368 };
369 }
370}