Skip to main content

rustdf/data/
utility.rs

1use byteorder::{ByteOrder, LittleEndian};
2use mscore::timstof::frame::TimsFrame;
3use rayon::iter::IntoParallelRefIterator;
4use rayon::prelude::*;
5use rayon::ThreadPoolBuilder;
6use std::io;
7use std::io::{Read, Write};
8
9/// Decompresses a ZSTD compressed byte array
10///
11/// # Arguments
12///
13/// * `compressed_data` - A byte slice that holds the compressed data
14///
15/// # Returns
16///
17/// * `decompressed_data` - A vector of u8 that holds the decompressed data
18///
19pub fn zstd_decompress(compressed_data: &[u8]) -> io::Result<Vec<u8>> {
20    let mut decoder = zstd::Decoder::new(compressed_data)?;
21    let mut decompressed_data = Vec::new();
22    decoder.read_to_end(&mut decompressed_data)?;
23    Ok(decompressed_data)
24}
25
26/// Compresses a byte array using ZSTD
27///
28/// # Arguments
29///
30/// * `decompressed_data` - A byte slice that holds the decompressed data
31///
32/// # Returns
33///
34/// * `compressed_data` - A vector of u8 that holds the compressed data
35///
36pub fn zstd_compress(decompressed_data: &[u8], compression_level: i32) -> io::Result<Vec<u8>> {
37    let mut encoder = zstd::Encoder::new(Vec::new(), compression_level)?;
38    encoder.write_all(decompressed_data)?;
39    let compressed_data = encoder.finish()?;
40    Ok(compressed_data)
41}
42
43/// Deduplicate `(scan, tof)` pairs (summing their intensities) and return the
44/// arrays sorted ascending by `(scan, tof)`.
45///
46/// The Bruker `tdf_bin` layout requires this ordering: [`modify_tofs`] delta-
47/// encodes TOF within each scan (so TOFs must ascend within a scan) and
48/// [`get_peak_cnts`] walks scans assuming they ascend. The Python writer
49/// enforces the same invariant via an `np.unique` dedup + `np.lexsort((tof,
50/// scan))` before encoding; the Rust write path previously fed raw, unsorted
51/// frame data straight into the encoder, producing negative/garbage TOF deltas
52/// that vendor readers (e.g. DiaNN) reject. Mirroring the Python preprocessing
53/// here keeps the two writers byte-for-byte identical.
54fn sort_dedup_scan_tof(
55    scans: &[u32],
56    tofs: &[u32],
57    intensities: &[u32],
58) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
59    use std::collections::HashMap;
60    let mut acc: HashMap<(u32, u32), u64> = HashMap::with_capacity(scans.len());
61    for i in 0..scans.len() {
62        *acc.entry((scans[i], tofs[i])).or_insert(0) += intensities[i] as u64;
63    }
64    let mut pairs: Vec<((u32, u32), u64)> = acc.into_iter().collect();
65    // Sort by scan, then tof — matches numpy's lexsort((tof, scan)).
66    pairs.sort_unstable_by_key(|&((s, t), _)| (s, t));
67
68    let n = pairs.len();
69    let mut out_scan = Vec::with_capacity(n);
70    let mut out_tof = Vec::with_capacity(n);
71    let mut out_int = Vec::with_capacity(n);
72    for ((s, t), inten) in pairs {
73        out_scan.push(s);
74        out_tof.push(t);
75        out_int.push(inten.min(u32::MAX as u64) as u32);
76    }
77    (out_scan, out_tof, out_int)
78}
79
80pub fn reconstruct_compressed_data(
81    scans: Vec<u32>,
82    tofs: Vec<u32>,
83    intensities: Vec<u32>,
84    total_scans: u32,
85    compression_level: i32,
86) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
87    // Ensuring all vectors have the same length
88    assert_eq!(scans.len(), tofs.len());
89    assert_eq!(scans.len(), intensities.len());
90
91    // Dedup + sort by (scan, tof) so TOF delta-encoding stays monotonic.
92    let (scans, mut tofs, intensities) = sort_dedup_scan_tof(&scans, &tofs, &intensities);
93
94    // Modify TOFs based on scans
95    modify_tofs(&mut tofs, &scans);
96
97    // Get peak counts from total scans and scans
98    let peak_cnts = get_peak_cnts(total_scans, &scans);
99
100    // Interleave TOFs and intensities
101    let mut interleaved = Vec::new();
102    for (&tof, &intensity) in tofs.iter().zip(intensities.iter()) {
103        interleaved.push(tof);
104        interleaved.push(intensity);
105    }
106
107    // Get real data using the custom loop logic
108    let real_data = get_realdata(&peak_cnts, &interleaved);
109
110    // Compress real_data using zstd_compress
111    let compressed_data = zstd_compress(&real_data, compression_level)?;
112
113    // Final data preparation with compressed data
114    let mut final_data = Vec::new();
115
116    // Include the length of the compressed data as a header (4 bytes)
117    final_data.extend_from_slice(&(compressed_data.len() as u32 + 8).to_le_bytes());
118
119    // Include total_scans as part of the header
120    final_data.extend_from_slice(&total_scans.to_le_bytes());
121
122    // Include the compressed data itself
123    final_data.extend_from_slice(&compressed_data);
124
125    Ok(final_data)
126}
127
128pub fn compress_collection(
129    frames: Vec<TimsFrame>,
130    max_scan_count: u32,
131    compression_level: i32,
132    num_threads: usize,
133) -> Vec<Vec<u8>> {
134    let pool = ThreadPoolBuilder::new()
135        .num_threads(num_threads)
136        .build()
137        .unwrap();
138
139    let result = pool.install(|| {
140        frames
141            .par_iter()
142            .map(|frame| {
143                let compressed_data = reconstruct_compressed_data(
144                    frame.scan.iter().map(|&x| x as u32).collect(),
145                    frame.tof.iter().map(|&x| x as u32).collect(),
146                    frame
147                        .ims_frame
148                        .intensity
149                        .iter()
150                        .map(|&x| x as u32)
151                        .collect(),
152                    max_scan_count,
153                    compression_level,
154                )
155                .unwrap();
156                compressed_data
157            })
158            .collect()
159    });
160    result
161}
162
163/// Parses the decompressed bruker binary data
164///
165/// # Arguments
166///
167/// * `decompressed_bytes` - A byte slice that holds the decompressed data
168///
169/// # Returns
170///
171/// * `scan_indices` - A vector of u32 that holds the scan indices
172/// * `tof_indices` - A vector of u32 that holds the tof indices
173/// * `intensities` - A vector of u32 that holds the intensities
174///
175pub fn parse_decompressed_bruker_binary_data(
176    decompressed_bytes: &[u8],
177) -> Result<(Vec<u32>, Vec<u32>, Vec<u32>), Box<dyn std::error::Error>> {
178    let mut buffer_u32 = Vec::new();
179
180    for i in 0..(decompressed_bytes.len() / 4) {
181        let value = LittleEndian::read_u32(&[
182            decompressed_bytes[i],
183            decompressed_bytes[i + (decompressed_bytes.len() / 4)],
184            decompressed_bytes[i + (2 * decompressed_bytes.len() / 4)],
185            decompressed_bytes[i + (3 * decompressed_bytes.len() / 4)],
186        ]);
187        buffer_u32.push(value);
188    }
189
190    // get the number of scans
191    let scan_count = buffer_u32[0] as usize;
192
193    // get the scan indices
194    let mut scan_indices: Vec<u32> = buffer_u32[..scan_count].to_vec();
195    for index in &mut scan_indices {
196        *index /= 2;
197    }
198
199    // first scan index is always 0?
200    scan_indices[0] = 0;
201
202    // get the tof indices, which are the first half of the buffer after the scan indices
203    let mut tof_indices: Vec<u32> = buffer_u32
204        .iter()
205        .skip(scan_count)
206        .step_by(2)
207        .cloned()
208        .collect();
209
210    // get the intensities, which are the second half of the buffer
211    let intensities: Vec<u32> = buffer_u32
212        .iter()
213        .skip(scan_count + 1)
214        .step_by(2)
215        .cloned()
216        .collect();
217
218    // calculate the last scan before moving scan indices
219    let last_scan = intensities.len() as u32 - scan_indices[1..].iter().sum::<u32>();
220
221    // shift the scan indices to the right
222    for i in 0..(scan_indices.len() - 1) {
223        scan_indices[i] = scan_indices[i + 1];
224    }
225
226    // set the last scan index
227    let len = scan_indices.len();
228    scan_indices[len - 1] = last_scan;
229
230    // convert the tof indices to cumulative sums
231    let mut index = 0;
232    for &size in &scan_indices {
233        let mut current_sum = 0;
234        for _ in 0..size {
235            current_sum += tof_indices[index];
236            tof_indices[index] = current_sum;
237            index += 1;
238        }
239    }
240
241    // adjust the tof indices to be zero-indexed
242    let adjusted_tof_indices: Vec<u32> = tof_indices.iter().map(|&val| val - 1).collect();
243    Ok((scan_indices, adjusted_tof_indices, intensities))
244}
245
246pub fn get_peak_cnts(total_scans: u32, scans: &[u32]) -> Vec<u32> {
247    let mut peak_cnts = vec![total_scans];
248    let mut ii = 0;
249    for scan_id in 1..total_scans {
250        let mut counter = 0;
251        while ii < scans.len() && scans[ii] < scan_id {
252            ii += 1;
253            counter += 1;
254        }
255        peak_cnts.push(counter * 2);
256    }
257    peak_cnts
258}
259
260pub fn modify_tofs(tofs: &mut [u32], scans: &[u32]) {
261    let mut last_tof = -1i32; // Using i32 to allow -1
262    let mut last_scan = 0;
263    for ii in 0..tofs.len() {
264        if last_scan != scans[ii] {
265            last_tof = -1;
266            last_scan = scans[ii];
267        }
268        let val = tofs[ii] as i32; // Cast to i32 for calculation
269        tofs[ii] = (val - last_tof) as u32; // Cast back to u32
270        last_tof = val;
271    }
272}
273
274pub fn get_realdata(peak_cnts: &[u32], interleaved: &[u32]) -> Vec<u8> {
275    let mut back_data = Vec::new();
276
277    // Convert peak counts to bytes and add to back_data
278    for &cnt in peak_cnts {
279        back_data.extend_from_slice(&cnt.to_le_bytes());
280    }
281
282    // Convert interleaved data to bytes and add to back_data
283    for &value in interleaved {
284        back_data.extend_from_slice(&value.to_le_bytes());
285    }
286
287    // Call get_realdata_loop for data rearrangement
288    get_realdata_loop(&back_data)
289}
290
291pub fn get_realdata_loop(back_data: &[u8]) -> Vec<u8> {
292    let mut real_data = vec![0u8; back_data.len()];
293    let mut reminder = 0;
294    let mut bd_idx = 0;
295    for rd_idx in 0..back_data.len() {
296        if bd_idx >= back_data.len() {
297            reminder += 1;
298            bd_idx = reminder;
299        }
300        real_data[rd_idx] = back_data[bd_idx];
301        bd_idx += 4;
302    }
303    real_data
304}
305
306pub fn get_data_for_compression(
307    tofs: &Vec<u32>,
308    scans: &Vec<u32>,
309    intensities: &Vec<u32>,
310    max_scans: u32,
311) -> Vec<u8> {
312    // Dedup + sort by (scan, tof) so TOF delta-encoding stays monotonic.
313    let (scans, tofs, intensities) = sort_dedup_scan_tof(scans, tofs, intensities);
314
315    let mut tof_copy = tofs.clone();
316    modify_tofs(&mut tof_copy, &scans);
317    let peak_cnts = get_peak_cnts(max_scans, &scans);
318    // Interleave the delta-encoded TOFs (`tof_copy`), not the raw `tofs`.
319    let interleaved: Vec<u32> = tof_copy
320        .iter()
321        .zip(intensities.iter())
322        .flat_map(|(tof, intensity)| vec![*tof, *intensity])
323        .collect();
324
325    get_realdata(&peak_cnts, &interleaved)
326}
327
328pub fn get_data_for_compression_par(
329    tofs: Vec<Vec<u32>>,
330    scans: Vec<Vec<u32>>,
331    intensities: Vec<Vec<u32>>,
332    max_scans: u32,
333    num_threads: usize,
334) -> Vec<Vec<u8>> {
335    let pool = ThreadPoolBuilder::new()
336        .num_threads(num_threads)
337        .build()
338        .unwrap();
339
340    let result = pool.install(|| {
341        tofs.par_iter()
342            .zip(scans.par_iter())
343            .zip(intensities.par_iter())
344            .map(|((tof, scan), intensity)| {
345                get_data_for_compression(tof, scan, intensity, max_scans)
346            })
347            .collect()
348    });
349
350    result
351}
352
353pub fn flatten_scan_values(scan: &Vec<u32>, zero_indexed: bool) -> Vec<u32> {
354    let add = if zero_indexed { 0 } else { 1 };
355    scan.iter()
356        .enumerate()
357        .flat_map(|(index, &count)| vec![(index + add) as u32; count as usize].into_iter())
358        .collect()
359}
360
361// Merge and sort inclusive integer ranges like [(3,7), (8,12), (20,25)].
362pub fn merge_ranges(mut ranges: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
363    if ranges.is_empty() { return ranges; }
364    ranges.sort_unstable_by_key(|x| x.0);
365    let mut out: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
366    let mut cur = ranges[0];
367    for (l, r) in ranges.into_iter().skip(1) {
368        if l <= cur.1 + 1 {
369            cur.1 = cur.1.max(r);
370        } else {
371            out.push(cur);
372            cur = (l, r);
373        }
374    }
375    out.push(cur);
376    out
377}