1use std::hash::{DefaultHasher, Hash, Hasher};
2use rayon::prelude::*;
3use rayon::ThreadPoolBuilder;
4use serde::{Deserialize, Serialize};
5use mscore::timstof::frame::TimsFrame;
6use mscore::timstof::slice::TimsSlice;
7use crate::cluster::peak::{FrameBinView, ImPeak1D, RtFrames, RtPeak1D};
8use crate::cluster::utility::{Fit1D, TofScale, fit1d_moment};
9use crate::data::handle::IndexConverter;
10
11fn compute_cluster_id_from_spec(spec: &ClusterSpec1D) -> u64 {
12 let mut h = DefaultHasher::new();
13
14 spec.rt_lo.hash(&mut h);
16 spec.rt_hi.hash(&mut h);
17 spec.im_lo.hash(&mut h);
18 spec.im_hi.hash(&mut h);
19 spec.tof_win.0.hash(&mut h);
20 spec.tof_win.1.hash(&mut h);
21 spec.tof_hist_bins.hash(&mut h);
22 spec.ms_level.hash(&mut h);
23
24 spec.window_group.unwrap_or(0).hash(&mut h);
26 spec.parent_im_id.unwrap_or(-1).hash(&mut h);
27 spec.parent_rt_id.unwrap_or(-1).hash(&mut h);
28
29 let sigma_bits: u32 = spec
31 .im_prior_sigma
32 .map(|s| s.to_bits())
33 .unwrap_or(0u32);
34 sigma_bits.hash(&mut h);
35
36 h.finish()
37}
38
39#[derive(Copy, Clone, Debug)]
40pub struct ScanSlice {
41 pub scan: usize,
42 pub start: usize,
43 pub end: usize
44}
45
46pub fn build_scan_slices(fr: &TimsFrame) -> Vec<ScanSlice> {
47 let scv = &fr.scan;
48 let mut out = Vec::new();
49 if scv.is_empty() { return out; }
50 let mut s_cur = scv[0];
51 let mut i_start = 0usize;
52 for i in 1..scv.len() {
53 if scv[i] != s_cur {
54 if s_cur >= 0 {
55 out.push(ScanSlice { scan: s_cur as usize, start: i_start, end: i });
56 }
57 s_cur = scv[i];
58 i_start = i;
59 }
60 }
61 if s_cur >= 0 {
62 out.push(ScanSlice { scan: s_cur as usize, start: i_start, end: scv.len() });
63 }
64 out
65}
66
67pub struct RawAttachContext {
68 pub slice: TimsSlice,
69 pub scan_slices: Vec<Vec<ScanSlice>>,
70 pub frame_ids_local: Vec<u32>,
71 pub rt_axis_sec: Vec<f32>,
72}
73
74#[derive(Clone, Debug, Default)]
75pub struct Attach1DOptions {
76 pub attach_points: bool,
77 pub attach_axes: bool,
78 pub max_points: Option<usize>, }
80
81impl Attach1DOptions {
82 pub fn default() -> Self {
83 Self {
84 attach_points: false,
85 attach_axes: false,
86 max_points: None,
87 }
88 }
89}
90
91#[derive(Clone, Debug)]
92pub struct Eval1DOpts {
93 pub refine_tof_once: bool,
95 pub refine_k_sigma: f32,
97 pub attach_axes: bool,
99 pub attach_rt_trace: bool,
101 pub attach_im_trace: bool,
102 pub attach: Attach1DOptions,
104 pub compute_mz_from_tof: bool,
105
106 pub pad_rt_frames: usize,
108 pub pad_im_scans: usize,
109 pub pad_tof_bins: usize,
110}
111
112impl Default for Eval1DOpts {
113 fn default() -> Self {
114 Self {
115 refine_tof_once: true,
116 refine_k_sigma: 3.0,
117 attach_axes: false,
118 attach_rt_trace: false,
119 attach_im_trace: false,
120 attach: Attach1DOptions::default(),
121 compute_mz_from_tof: false,
122 pad_rt_frames: 0,
123 pad_im_scans: 0,
124 pad_tof_bins: 0,
125 }
126 }
127}
128
129#[derive(Clone, Debug)]
130pub struct ClusterSpec1D {
131 pub rt_lo: usize,
133 pub rt_hi: usize,
134 pub im_lo: usize,
136 pub im_hi: usize,
137 pub tof_win: (i32, i32),
139 pub tof_hist_bins: usize,
141
142 pub window_group: Option<u32>,
144 pub parent_im_id: Option<i64>,
145 pub parent_rt_id: Option<i64>,
146 pub ms_level: u8,
147
148 pub im_prior_sigma: Option<f32>,
150}
151
152#[derive(Clone, Debug, Default, Serialize, Deserialize)]
153pub struct RawPoints {
154 pub mz: Vec<f32>, pub rt: Vec<f32>,
156 pub im: Vec<f32>,
157 pub scan: Vec<u32>,
158 pub intensity: Vec<f32>,
159 pub tof: Vec<i32>,
160 pub frame: Vec<u32>,
161}
162
163#[derive(Clone, Debug, Default, Serialize, Deserialize)]
169pub struct Windows1D {
170 pub rt: (usize, usize),
171 pub im: (usize, usize),
172 pub tof: (usize, usize),
173 pub tof_index: (i32, i32),
175 pub mz: Option<(f32, f32)>,
177}
178
179#[derive(Clone, Debug, Default, Serialize, Deserialize)]
181pub struct AxisFits {
182 pub rt: Fit1D,
183 pub im: Fit1D,
184 pub tof: Fit1D,
185 pub mz: Option<Fit1D>,
187}
188
189#[derive(Clone, Debug, Default, Serialize, Deserialize)]
191pub struct ClusterRawData {
192 pub raw_points: Option<RawPoints>,
193 pub rt_axis_sec: Option<Vec<f32>>,
194 pub im_axis_scans: Option<Vec<usize>>,
195 pub mz_axis_da: Option<Vec<f32>>,
196 #[serde(default)]
197 pub rt_trace: Option<Vec<f32>>,
198 #[serde(default)]
199 pub im_trace: Option<Vec<f32>>,
200}
201
202#[derive(Clone, Debug, Default, Serialize, Deserialize)]
204pub struct ClusterProvenance {
205 pub window_group: Option<u32>,
206 pub parent_im_id: Option<i64>,
207 pub parent_rt_id: Option<i64>,
208 pub ms_level: u8,
209}
210
211#[derive(Clone, Debug, Default, Serialize, Deserialize)]
213pub struct IntensityMetrics {
214 pub raw_sum: f32,
215 pub volume_proxy: f32,
216}
217
218#[derive(Clone, Debug, Serialize, Deserialize)]
219pub struct ClusterResult1D {
220 pub cluster_id: u64,
221 pub rt_window: (usize, usize),
222 pub im_window: (usize, usize),
223 pub tof_window: (usize, usize),
224 pub tof_index_window: (i32, i32),
225
226 pub mz_window: Option<(f32, f32)>,
228
229 pub rt_fit: Fit1D,
230 pub im_fit: Fit1D,
231 pub tof_fit: Fit1D,
233 pub mz_fit: Option<Fit1D>,
235
236 pub raw_sum: f32,
237 pub volume_proxy: f32,
238
239 pub frame_ids_used: Vec<u32>,
240 pub window_group: Option<u32>,
241 pub parent_im_id: Option<i64>,
242 pub parent_rt_id: Option<i64>,
243 pub ms_level: u8,
244
245 pub rt_axis_sec: Option<Vec<f32>>,
246 pub im_axis_scans: Option<Vec<usize>>,
247 pub mz_axis_da: Option<Vec<f32>>,
249
250 pub raw_points: Option<RawPoints>,
252
253 #[serde(default)]
255 pub rt_trace: Option<Vec<f32>>,
256
257 #[serde(default)]
259 pub im_trace: Option<Vec<f32>>,
260}
261
262impl ClusterResult1D {
263 pub fn merged_with(
264 &self,
265 other: &Self,
266 new_id: u64,
267 policy: &ClusterMergePolicy,
268 ) -> Self {
269 merge_clusters(self, other, new_id, policy)
270 }
271
272 #[inline]
276 pub fn windows(&self) -> Windows1D {
277 Windows1D {
278 rt: self.rt_window,
279 im: self.im_window,
280 tof: self.tof_window,
281 tof_index: self.tof_index_window,
282 mz: self.mz_window,
283 }
284 }
285
286 #[inline]
288 pub fn fits(&self) -> AxisFits {
289 AxisFits {
290 rt: self.rt_fit.clone(),
291 im: self.im_fit.clone(),
292 tof: self.tof_fit.clone(),
293 mz: self.mz_fit.clone(),
294 }
295 }
296
297 #[inline]
299 pub fn raw_data(&self) -> ClusterRawData {
300 ClusterRawData {
301 raw_points: self.raw_points.clone(),
302 rt_axis_sec: self.rt_axis_sec.clone(),
303 im_axis_scans: self.im_axis_scans.clone(),
304 mz_axis_da: self.mz_axis_da.clone(),
305 rt_trace: self.rt_trace.clone(),
306 im_trace: self.im_trace.clone(),
307 }
308 }
309
310 #[inline]
312 pub fn provenance(&self) -> ClusterProvenance {
313 ClusterProvenance {
314 window_group: self.window_group,
315 parent_im_id: self.parent_im_id,
316 parent_rt_id: self.parent_rt_id,
317 ms_level: self.ms_level,
318 }
319 }
320
321 #[inline]
323 pub fn intensity(&self) -> IntensityMetrics {
324 IntensityMetrics {
325 raw_sum: self.raw_sum,
326 volume_proxy: self.volume_proxy,
327 }
328 }
329
330 #[inline]
332 pub fn has_raw_data(&self) -> bool {
333 self.raw_points.is_some()
334 || self.rt_axis_sec.is_some()
335 || self.im_axis_scans.is_some()
336 || self.mz_axis_da.is_some()
337 || self.rt_trace.is_some()
338 || self.im_trace.is_some()
339 }
340
341 #[inline]
343 pub fn has_valid_fits(&self) -> bool {
344 self.rt_fit.sigma > 0.0 && self.im_fit.sigma > 0.0 && self.tof_fit.sigma > 0.0
345 }
346}
347
348impl Windows1D {
351 #[inline]
353 pub fn overlaps(&self, other: &Self) -> bool {
354 let rt_overlaps = self.rt.1 >= other.rt.0 && other.rt.1 >= self.rt.0;
355 let im_overlaps = self.im.1 >= other.im.0 && other.im.1 >= self.im.0;
356 let tof_overlaps = self.tof.1 >= other.tof.0 && other.tof.1 >= self.tof.0;
357 rt_overlaps && im_overlaps && tof_overlaps
358 }
359
360 #[inline]
362 pub fn merge(&self, other: &Self) -> Self {
363 Self {
364 rt: (self.rt.0.min(other.rt.0), self.rt.1.max(other.rt.1)),
365 im: (self.im.0.min(other.im.0), self.im.1.max(other.im.1)),
366 tof: (self.tof.0.min(other.tof.0), self.tof.1.max(other.tof.1)),
367 tof_index: (
368 self.tof_index.0.min(other.tof_index.0),
369 self.tof_index.1.max(other.tof_index.1),
370 ),
371 mz: match (self.mz, other.mz) {
372 (Some((a, b)), Some((c, d))) => Some((a.min(c), b.max(d))),
373 (Some(x), None) | (None, Some(x)) => Some(x),
374 (None, None) => None,
375 },
376 }
377 }
378}
379
380impl AxisFits {
381 #[inline]
383 pub fn within_distance(&self, other: &Self, rt_tol: f32, im_tol: f32, tof_tol: f32) -> bool {
384 let drt = (self.rt.mu - other.rt.mu).abs();
385 let dim = (self.im.mu - other.im.mu).abs();
386 let dtof = (self.tof.mu - other.tof.mu).abs();
387 drt <= rt_tol && dim <= im_tol && dtof <= tof_tol
388 }
389}
390
391impl IntensityMetrics {
392 #[inline]
394 pub fn from_raw_sum(raw_sum: f32) -> Self {
395 Self {
396 raw_sum,
397 volume_proxy: raw_sum,
398 }
399 }
400}
401
402pub fn decorate_with_mz_for_cluster<D: IndexConverter>(
403 ds: &D,
404 rt_frames: &RtFrames,
405 res: &mut ClusterResult1D,
406) {
407 let n_frames = rt_frames.frame_ids.len();
408 if n_frames == 0 {
409 return;
410 }
411
412 let scale = &*rt_frames.scale;
413
414 let (mut rt_lo, mut rt_hi) = res.rt_window;
418 if rt_lo > rt_hi {
419 std::mem::swap(&mut rt_lo, &mut rt_hi);
420 }
421 if rt_lo >= n_frames {
422 return;
423 }
424 rt_hi = rt_hi.min(n_frames.saturating_sub(1));
425 if rt_lo > rt_hi {
426 return;
427 }
428
429 let mid_local = (rt_lo + rt_hi) / 2;
430 let frame_id = rt_frames.frame_ids[mid_local];
431
432 let (mut bin_lo, mut bin_hi) = res.tof_window;
436 if bin_lo > bin_hi {
437 std::mem::swap(&mut bin_lo, &mut bin_hi);
438 }
439
440 let n_bins = scale.num_bins();
441 if n_bins == 0 {
442 return;
443 }
444 bin_lo = bin_lo.min(n_bins.saturating_sub(1));
445 bin_hi = bin_hi.min(n_bins.saturating_sub(1));
446 if bin_lo > bin_hi {
447 return;
448 }
449
450 let tof_lo_idx = scale.center(bin_lo).floor().max(0.0) as u32;
451 let tof_hi_idx = scale.center(bin_hi).ceil().max(0.0) as u32;
452
453 let tof_pair: Vec<u32> = vec![tof_lo_idx, tof_hi_idx];
457 let mz_pair = ds.tof_to_mz(frame_id, &tof_pair);
458 if mz_pair.len() != 2 {
459 return;
460 }
461
462 let mut mz_lo = mz_pair[0] as f32;
463 let mut mz_hi = mz_pair[1] as f32;
464 if !mz_lo.is_finite() || !mz_hi.is_finite() {
465 return;
466 }
467 if mz_lo > mz_hi {
468 std::mem::swap(&mut mz_lo, &mut mz_hi);
469 }
470
471 res.mz_window = Some((mz_lo, mz_hi));
472
473 let tof_centers: Vec<u32> = (bin_lo..=bin_hi)
477 .map(|b| scale.center(b).round().max(0.0) as u32)
478 .collect();
479
480 let mz_axis_f64 = ds.tof_to_mz(frame_id, &tof_centers);
481 if mz_axis_f64.len() == tof_centers.len() && !mz_axis_f64.is_empty() {
482 let mz_axis_da: Vec<f32> = mz_axis_f64.iter().map(|&x| x as f32).collect();
483 res.mz_axis_da = Some(mz_axis_da.clone());
484
485 if res.tof_fit.mu.is_finite() && res.tof_fit.sigma.is_finite() && res.tof_fit.sigma > 0.0 {
489 let tof_mu = res.tof_fit.mu.max(0.0).round() as u32;
490 let tof_minus = (res.tof_fit.mu - res.tof_fit.sigma).max(0.0).round() as u32;
491 let tof_plus = (res.tof_fit.mu + res.tof_fit.sigma).max(0.0).round() as u32;
492
493 let tof_triplet: Vec<u32> = vec![tof_mu, tof_minus, tof_plus];
494 let mz_triplet = ds.tof_to_mz(frame_id, &tof_triplet);
495 if mz_triplet.len() == 3 {
496 let mu_mz = mz_triplet[0] as f32;
497 let sigma_mz = ((mz_triplet[2] - mz_triplet[1]).abs() as f32 * 0.5).max(1e-6);
498
499 let mut mz_fit = res.tof_fit.clone();
500 mz_fit.mu = mu_mz;
501 mz_fit.sigma = sigma_mz;
502
503 res.mz_fit = Some(mz_fit);
504 }
505 }
506 }
507}
508#[inline]
513fn sum_frame_block(
514 fbv: &FrameBinView,
515 bin_lo: usize,
516 bin_hi: usize,
517 im_lo: usize,
518 im_hi: usize,
519) -> f32 {
520 let ub = &fbv.unique_bins;
521 if ub.is_empty() || bin_lo > bin_hi {
522 return 0.0;
523 }
524
525 let start = match ub.binary_search(&bin_lo) {
526 Ok(i) => i,
527 Err(i) => i.min(ub.len()),
528 };
529
530 let mut acc = 0.0f32;
531 let mut i = start;
532 while i < ub.len() {
533 let b = ub[i];
534 if b > bin_hi {
535 break;
536 }
537
538 let lo = fbv.offsets[i];
539 let hi = fbv.offsets[i + 1];
540 let scans = &fbv.scan_idx[lo..hi];
541 let ints = &fbv.intensity[lo..hi];
542
543 for (s, val) in scans.iter().zip(ints.iter()) {
544 let s = *s as usize;
545 if s >= im_lo && s <= im_hi {
546 acc += *val;
547 }
548 }
549
550 i += 1;
551 }
552
553 acc
554}
555
556pub fn build_rt_marginal(
558 frames: &[FrameBinView],
559 rt_lo: usize,
560 rt_hi: usize,
561 bin_lo: usize,
562 bin_hi: usize,
563 im_lo: usize,
564 im_hi: usize,
565) -> Vec<f32> {
566 let mut out = vec![0.0f32; rt_hi + 1 - rt_lo];
567 for (k, fbv) in frames[rt_lo..=rt_hi].iter().enumerate() {
568 out[k] = sum_frame_block(fbv, bin_lo, bin_hi, im_lo, im_hi);
569 }
570 out
571}
572
573pub fn build_im_marginal(
575 frames: &[FrameBinView],
576 rt_lo: usize,
577 rt_hi: usize,
578 bin_lo: usize,
579 bin_hi: usize,
580 im_lo: usize,
581 im_hi: usize,
582) -> Vec<f32> {
583 let len = im_hi + 1 - im_lo;
584 let mut out = vec![0.0f32; len];
585
586 for fbv in &frames[rt_lo..=rt_hi] {
587 let ub = &fbv.unique_bins;
588 if ub.is_empty() {
589 continue;
590 }
591
592 let start = match ub.binary_search(&bin_lo) {
593 Ok(i) => i,
594 Err(i) => i.min(ub.len()),
595 };
596
597 let mut i = start;
598 while i < ub.len() {
599 let b = ub[i];
600 if b > bin_hi {
601 break;
602 }
603 let lo = fbv.offsets[i];
604 let hi = fbv.offsets[i + 1];
605 let scans = &fbv.scan_idx[lo..hi];
606 let ints = &fbv.intensity[lo..hi];
607
608 for (s, val) in scans.iter().zip(ints.iter()) {
609 let s_abs = *s as usize;
610 if s_abs >= im_lo && s_abs <= im_hi {
611 out[s_abs - im_lo] += *val;
612 }
613 }
614
615 i += 1;
616 }
617 }
618
619 out
620}
621
622pub fn build_tof_hist(
627 frames: &[FrameBinView],
628 rt_lo: usize,
629 rt_hi: usize,
630 bin_lo: usize,
631 bin_hi: usize,
632 im_lo: usize,
633 im_hi: usize,
634 scale: &TofScale,
635) -> (Vec<f32>, Vec<f32>) {
636 let r = bin_hi + 1 - bin_lo;
637 let mut hist = vec![0.0f32; r];
638
639 for fbv in &frames[rt_lo..=rt_hi] {
640 let ub = &fbv.unique_bins;
641 if ub.is_empty() {
642 continue;
643 }
644
645 let start = match ub.binary_search(&bin_lo) {
646 Ok(i) => i,
647 Err(i) => i.min(ub.len()),
648 };
649
650 let mut i = start;
651 while i < ub.len() {
652 let b = ub[i];
653 if b > bin_hi {
654 break;
655 }
656
657 let lo = fbv.offsets[i];
658 let hi = fbv.offsets[i + 1];
659
660 let scans = &fbv.scan_idx[lo..hi];
661 let ints = &fbv.intensity[lo..hi];
662
663 let mut sum = 0.0f32;
664 for (s, val) in scans.iter().zip(ints.iter()) {
665 let s_abs = *s as usize;
666 if s_abs >= im_lo && s_abs <= im_hi {
667 sum += *val;
668 }
669 }
670
671 hist[b - bin_lo] += sum;
672 i += 1;
673 }
674 }
675
676 let centers = (bin_lo..=bin_hi).map(|i| scale.center(i)).collect::<Vec<_>>();
677 (hist, centers)
678}
679
680#[inline]
681fn sanitize_fit(
682 f: &mut Fit1D,
683 mu_bounds: Option<(f32, f32)>,
684 min_sigma: f32,
685 enforce_nonneg: bool,
686) {
687 if !f.mu.is_finite() {
689 if let Some((lo, hi)) = mu_bounds {
690 if lo.is_finite() && hi.is_finite() && lo <= hi {
691 f.mu = if hi > lo { 0.5 * (lo + hi) } else { lo };
692 } else {
693 f.mu = 0.0;
694 }
695 } else {
696 f.mu = 0.0;
697 }
698 }
699 if !f.sigma.is_finite() {
701 f.sigma = 0.0;
702 }
703 if !f.height.is_finite() {
704 f.height = 0.0;
705 }
706 if !f.baseline.is_finite() {
707 f.baseline = 0.0;
708 }
709 if !f.area.is_finite() {
710 f.area = 0.0;
711 }
712 if !f.r2.is_finite() {
713 f.r2 = 0.0;
714 }
715
716 if let Some((lo, hi)) = mu_bounds {
717 if lo.is_finite() && hi.is_finite() && lo < hi {
718 if f.mu < lo {
719 f.mu = lo;
720 }
721 if f.mu > hi {
722 f.mu = hi;
723 }
724 }
725 }
726
727 if f.sigma < 0.0 {
728 f.sigma = 0.0;
729 }
730 if f.sigma > 0.0 && f.sigma < min_sigma {
731 f.sigma = min_sigma;
732 }
733
734 if enforce_nonneg {
735 if f.baseline < 0.0 {
736 f.baseline = 0.0;
737 }
738 if f.height < 0.0 {
739 f.height = 0.0;
740 }
741 if f.area < 0.0 {
742 f.area = 0.0;
743 }
744 }
745}
746
747#[inline]
748fn argmax_idx(v: &[f32]) -> usize {
749 let mut i_max = 0usize;
750 let mut best = f32::NEG_INFINITY;
751 for (i, &y) in v.iter().enumerate() {
752 if y > best {
753 best = y;
754 i_max = i;
755 }
756 }
757 i_max
758}
759
760#[inline]
765fn rt_overlap((a0,a1):(usize, usize),(b0,b1):(usize,usize)) -> usize {
766 if a0 > a1 || b0 > b1 { return 0; }
767 let lo = a0.max(b0);
768 let hi = a1.min(b1);
769 hi.saturating_sub(lo).saturating_add(1)
770}
771
772fn rt_bounds_from_im(im: &ImPeak1D, n_frames: usize) -> (usize, usize) {
775 let (a,b) = im.rt_bounds; let mut lo = a.min(n_frames.saturating_sub(1));
777 let mut hi = b.min(n_frames.saturating_sub(1));
778 if lo > hi { std::mem::swap(&mut lo, &mut hi); }
779 (lo, hi)
780}
781
782#[inline]
784pub fn bin_range_for_win(_scale: &TofScale, tof_win: (i32, i32)) -> (usize, usize) {
785 let (a, b) = tof_win;
786 let mut lo = a.min(b).max(0) as usize;
787 let mut hi = a.max(b).max(0) as usize;
788 if lo > hi {
789 std::mem::swap(&mut lo, &mut hi);
790 }
791 (lo, hi)
792}
793
794pub fn evaluate_spec_1d(
799 rt_frames: &RtFrames,
800 spec: &ClusterSpec1D,
801 opts: &Eval1DOpts,
802) -> ClusterResult1D {
803 debug_assert!(rt_frames.is_consistent());
804 let scale = &*rt_frames.scale;
805
806 let n_frames = rt_frames.frames.len();
807 let n_bins = scale.num_bins();
808
809 let mut rt_lo_eff = spec.rt_lo.min(n_frames.saturating_sub(1));
815 let mut rt_hi_eff = spec.rt_hi.min(n_frames.saturating_sub(1));
816 if rt_lo_eff > rt_hi_eff {
817 std::mem::swap(&mut rt_lo_eff, &mut rt_hi_eff);
818 }
819 if opts.pad_rt_frames > 0 && n_frames > 0 {
820 let pad = opts.pad_rt_frames;
821 rt_lo_eff = rt_lo_eff.saturating_sub(pad);
822 rt_hi_eff = (rt_hi_eff + pad).min(n_frames.saturating_sub(1));
823 }
824
825 let mut im_lo_eff = spec.im_lo;
827 let mut im_hi_eff = spec.im_hi;
828 if im_lo_eff > im_hi_eff {
829 std::mem::swap(&mut im_lo_eff, &mut im_hi_eff);
830 }
831 if opts.pad_im_scans > 0 {
832 let pad = opts.pad_im_scans;
833 im_lo_eff = im_lo_eff.saturating_sub(pad);
834 im_hi_eff = im_hi_eff.saturating_add(pad);
835 }
836
837 let (mut bin_lo0, mut bin_hi0) = bin_range_for_win(scale, spec.tof_win);
839 if opts.pad_tof_bins > 0 && n_bins > 0 {
840 let pad = opts.pad_tof_bins.min(n_bins.saturating_sub(1));
841 bin_lo0 = bin_lo0.saturating_sub(pad);
842 bin_hi0 = (bin_hi0 + pad).min(n_bins.saturating_sub(1));
843 }
844
845 let rt_marg1 = build_rt_marginal(
847 &rt_frames.frames,
848 rt_lo_eff,
849 rt_hi_eff,
850 bin_lo0,
851 bin_hi0,
852 im_lo_eff,
853 im_hi_eff,
854 );
855 let im_marg1 = build_im_marginal(
856 &rt_frames.frames,
857 rt_lo_eff,
858 rt_hi_eff,
859 bin_lo0,
860 bin_hi0,
861 im_lo_eff,
862 im_hi_eff,
863 );
864 let (axis_hist1, axis_centers1) = build_tof_hist(
865 &rt_frames.frames,
866 rt_lo_eff,
867 rt_hi_eff,
868 bin_lo0,
869 bin_hi0,
870 im_lo_eff,
871 im_hi_eff,
872 scale,
873 );
874
875 let rt_sum1: f32 = rt_marg1.iter().copied().sum();
876 let im_sum1: f32 = im_marg1.iter().copied().sum();
877 let ax_sum1: f32 = axis_hist1.iter().copied().sum();
878
879 let cluster_id = compute_cluster_id_from_spec(spec);
880
881 let n_bins = scale.num_bins();
883 let mut bin_lo_idx = bin_lo0.min(n_bins.saturating_sub(1));
884 let mut bin_hi_idx = bin_hi0.min(n_bins.saturating_sub(1));
885 if bin_lo_idx > bin_hi_idx {
886 std::mem::swap(&mut bin_lo_idx, &mut bin_hi_idx);
887 }
888
889 let tof_idx_lo = scale.center(bin_lo_idx).floor().max(0.0) as i32;
891 let tof_idx_hi = scale.center(bin_hi_idx).ceil().max(0.0) as i32;
892
893 let tof_index_window = (tof_idx_lo, tof_idx_hi);
894
895 if rt_sum1 <= 0.0 || im_sum1 <= 0.0 || ax_sum1 <= 0.0 {
897 return ClusterResult1D {
898 cluster_id,
899 rt_window: (rt_lo_eff, rt_hi_eff),
900 im_window: (im_lo_eff, im_hi_eff),
901 tof_window: (bin_lo0, bin_hi0),
902 tof_index_window,
903
904 mz_window: None,
905 rt_fit: Fit1D::default(),
906 im_fit: Fit1D::default(),
907 tof_fit: Fit1D::default(),
908 mz_fit: None,
909
910 raw_sum: 0.0,
911 volume_proxy: 0.0,
912 frame_ids_used: rt_frames.frame_ids[rt_lo_eff..=rt_hi_eff].to_vec(),
913 window_group: spec.window_group,
914 parent_im_id: spec.parent_im_id,
915 parent_rt_id: spec.parent_rt_id,
916 ms_level: spec.ms_level,
917 rt_axis_sec: opts
918 .attach_axes
919 .then_some(rt_frames.rt_times[rt_lo_eff..=rt_hi_eff].to_vec()),
920 im_axis_scans: opts
921 .attach_axes
922 .then_some((im_lo_eff..=im_hi_eff).collect()),
923 mz_axis_da: None,
924 raw_points: None,
925 rt_trace: opts
926 .attach_rt_trace
927 .then_some(thin_f32_vec(&rt_marg1, None)),
928 im_trace: opts
929 .attach_im_trace
930 .then_some(thin_f32_vec(&im_marg1, None)),
931 };
932 }
933
934 let i_max1 = argmax_idx(&axis_hist1);
936
937 let (bin_lo, bin_hi, axis_centers2) = {
938 if opts.refine_tof_once {
939 let center_bin = bin_lo0 + i_max1; let half_span: usize = 4;
941
942 let bl = center_bin.saturating_sub(half_span).max(bin_lo0);
943 let bh = (center_bin + half_span).min(bin_hi0);
944
945 (bl, bh, (bl..=bh).map(|i| scale.center(i)).collect::<Vec<_>>())
946 } else {
947 (bin_lo0, bin_hi0, axis_centers1.clone())
948 }
949 };
950
951 let rt_marg = build_rt_marginal(
953 &rt_frames.frames,
954 rt_lo_eff,
955 rt_hi_eff,
956 bin_lo,
957 bin_hi,
958 im_lo_eff,
959 im_hi_eff,
960 );
961 let im_marg = build_im_marginal(
962 &rt_frames.frames,
963 rt_lo_eff,
964 rt_hi_eff,
965 bin_lo,
966 bin_hi,
967 im_lo_eff,
968 im_hi_eff,
969 );
970 let (axis_hist, _axis_centers_final) = build_tof_hist(
971 &rt_frames.frames,
972 rt_lo_eff,
973 rt_hi_eff,
974 bin_lo,
975 bin_hi,
976 im_lo_eff,
977 im_hi_eff,
978 scale,
979 );
980
981 let rt_sum: f32 = rt_marg.iter().copied().sum();
982 let im_sum: f32 = im_marg.iter().copied().sum();
983 let ax_sum: f32 = axis_hist.iter().copied().sum();
984
985 let (use_bin_lo, use_bin_hi, use_rt_marg, use_im_marg, use_axis_hist, use_axis_centers) =
986 if rt_sum <= 0.0 || im_sum <= 0.0 || ax_sum <= 0.0 {
987 (bin_lo0, bin_hi0, &rt_marg1, &im_marg1, &axis_hist1, &axis_centers1)
988 } else {
989 (bin_lo, bin_hi, &rt_marg, &im_marg, &axis_hist, &axis_centers2)
990 };
991
992 let rt_trace_opt = if opts.attach_rt_trace {
994 Some(thin_f32_vec(use_rt_marg, None))
995 } else {
996 None
997 };
998 let im_trace_opt = if opts.attach_im_trace {
999 Some(thin_f32_vec(use_im_marg, None))
1000 } else {
1001 None
1002 };
1003
1004 let rt_times: Vec<f32> = rt_frames.rt_times[rt_lo_eff..=rt_hi_eff].to_vec();
1007 let im_axis: Vec<usize> = (im_lo_eff..=im_hi_eff).collect();
1008 let im_axis_f32: Vec<f32> = im_axis.iter().map(|&s| s as f32).collect();
1009
1010 let mut rt_fit = fit1d_moment(use_rt_marg, Some(&rt_times));
1012 let mut im_fit = fit1d_moment(use_im_marg, Some(&im_axis_f32));
1013
1014 let axis_centers_f32: Vec<f32> = use_axis_centers.iter().copied().collect();
1016 let mut tof_fit = fit1d_moment(use_axis_hist, Some(&axis_centers_f32));
1017
1018 let rt_mu_bounds =
1020 if rt_lo_eff < rt_frames.rt_times.len() && rt_hi_eff < rt_frames.rt_times.len() {
1021 Some((rt_frames.rt_times[rt_lo_eff], rt_frames.rt_times[rt_hi_eff]))
1022 } else {
1023 None
1024 };
1025
1026 let axis_min = *use_axis_centers
1027 .first()
1028 .unwrap_or(&tof_fit.mu);
1029 let axis_max = *use_axis_centers
1030 .last()
1031 .unwrap_or(&tof_fit.mu);
1032 let axis_mu_bounds = Some((axis_min, axis_max));
1033 let im_mu_bounds = Some((im_lo_eff as f32, im_hi_eff as f32));
1034
1035 sanitize_fit(&mut tof_fit, axis_mu_bounds, 1e-6, true);
1037 sanitize_fit(&mut rt_fit, rt_mu_bounds, 1e-6, true);
1038 sanitize_fit(&mut im_fit, im_mu_bounds, 1e-3, true);
1039
1040 if im_fit.mu == 0.0 {
1041 let lo = im_lo_eff as f32;
1042 let hi = im_hi_eff as f32;
1043 im_fit.mu = if hi > lo { 0.5 * (lo + hi) } else { lo.max(1.0) };
1044 }
1045 if !im_fit.sigma.is_finite() || im_fit.sigma <= 0.0 {
1046 let k = opts.refine_k_sigma.max(1.0);
1047 let width = (im_hi_eff.saturating_sub(im_lo_eff) as f32) + 1.0;
1048 let from_window = ((width - 1.0) / (2.0 * k)).max(0.0);
1049 let prior = spec.im_prior_sigma.unwrap_or(0.0).max(0.0);
1050 let mut s = prior.max(from_window).max(1e-3);
1051 if !s.is_finite() {
1052 s = 1e-3;
1053 }
1054 im_fit.sigma = s;
1055 }
1056
1057 let raw_sum: f32 = use_rt_marg.iter().copied().sum();
1058 let volume_proxy = (rt_fit.area.max(0.0))
1059 * (im_fit.area.max(0.0))
1060 * (tof_fit.area.max(0.0));
1061
1062 let n_bins = scale.num_bins();
1064 let mut bin_lo_idx = use_bin_lo.min(n_bins.saturating_sub(1));
1065 let mut bin_hi_idx = use_bin_hi.min(n_bins.saturating_sub(1));
1066 if bin_lo_idx > bin_hi_idx {
1067 std::mem::swap(&mut bin_lo_idx, &mut bin_hi_idx);
1068 }
1069
1070 let tof_idx_lo = scale.center(bin_lo_idx).floor().max(0.0) as i32;
1072 let tof_idx_hi = scale.center(bin_hi_idx).ceil().max(0.0) as i32;
1073
1074 let tof_index_window = (tof_idx_lo, tof_idx_hi);
1075
1076 ClusterResult1D {
1077 cluster_id,
1078 rt_window: (rt_lo_eff, rt_hi_eff),
1079 im_window: (im_lo_eff, im_hi_eff),
1080 tof_window: (use_bin_lo, use_bin_hi),
1081 tof_index_window,
1082
1083 mz_window: None, rt_fit,
1085 im_fit,
1086 tof_fit,
1087 mz_fit: None, raw_sum,
1090 volume_proxy,
1091 frame_ids_used: rt_frames.frame_ids[rt_lo_eff..=rt_hi_eff].to_vec(),
1092 window_group: spec.window_group,
1093 parent_im_id: spec.parent_im_id,
1094 parent_rt_id: spec.parent_rt_id,
1095 ms_level: spec.ms_level,
1096 rt_axis_sec: opts.attach_axes.then_some(rt_times),
1097 im_axis_scans: opts.attach_axes.then_some(im_axis),
1098 mz_axis_da: None, raw_points: None,
1100 rt_trace: rt_trace_opt,
1101 im_trace: im_trace_opt,
1102 }
1103}
1104
1105#[derive(Clone, Copy, Debug)]
1110pub struct BuildSpecOpts {
1111 pub extra_rt_pad: usize,
1114
1115 pub extra_im_pad: usize,
1118
1119 pub tof_bin_pad: usize,
1122
1123 pub tof_hist_bins: usize,
1125
1126 pub ms_level: u8,
1128
1129 pub min_im_span: usize,
1131
1132 pub im_k_sigma: f32,
1135}
1136
1137impl BuildSpecOpts {
1138 #[inline]
1139 pub fn with_ms_level(self, level: u8) -> Self {
1140 let mut o = self;
1141 o.ms_level = level;
1142 o
1143 }
1144
1145 #[inline]
1146 pub fn ms1_defaults() -> Self {
1147 BuildSpecOpts {
1148 extra_rt_pad: 0,
1149 extra_im_pad: 0,
1150 tof_bin_pad: 0,
1151 tof_hist_bins: 16,
1152 ms_level: 1,
1153 min_im_span: 10,
1154 im_k_sigma: 3.0,
1155 }
1156 }
1157
1158 #[inline]
1159 pub fn ms2_defaults() -> Self {
1160 BuildSpecOpts {
1161 extra_rt_pad: 0,
1162 extra_im_pad: 0,
1163 tof_bin_pad: 0,
1164 tof_hist_bins: 16,
1165 ms_level: 2,
1166 min_im_span: 10,
1167 im_k_sigma: 3.0,
1168 }
1169 }
1170}
1171
1172#[inline]
1173fn widen_scan_window(lo: usize, hi: usize, want_span: usize, center: usize) -> (usize, usize) {
1174 let cur_span = hi.saturating_sub(lo).saturating_add(1);
1175 if cur_span >= want_span { return (lo, hi); }
1176 let half = want_span / 2;
1177 let new_lo = center.saturating_sub(half);
1178 let new_hi = center.saturating_add(want_span - 1 - half);
1179 (new_lo.min(lo), new_hi.max(hi)) }
1181
1182#[inline]
1183fn scans_from_sigma(k: f32, sigma: f32) -> usize {
1184 let k = k.max(0.0);
1186 let s = sigma.max(0.0);
1187 let span = (2.0 * k * s).ceil() as isize + 1;
1188 span.max(1) as usize
1189}
1190
1191#[inline]
1193fn axis_bin_range_for_bounds(scale: &TofScale, bounds: (i32, i32)) -> (usize, usize) {
1194 let (mut lo, mut hi) = bounds;
1195 if lo > hi {
1196 std::mem::swap(&mut lo, &mut hi);
1197 }
1198
1199 let n_bins = scale.num_bins();
1200 if n_bins == 0 {
1201 return (0, 0);
1202 }
1203
1204 let (mut bin_lo, mut bin_hi) = scale.index_range_for_tof_window(lo, hi);
1206
1207 bin_lo = bin_lo.min(n_bins.saturating_sub(1));
1209 bin_hi = bin_hi.min(n_bins.saturating_sub(1));
1210 if bin_lo > bin_hi {
1211 std::mem::swap(&mut bin_lo, &mut bin_hi);
1212 }
1213
1214 (bin_lo, bin_hi)
1215}
1216
1217pub fn make_spec_from_pair(
1218 im: &ImPeak1D,
1219 rt: &RtPeak1D,
1220 rt_frames: &RtFrames,
1221 opts: &BuildSpecOpts,
1222) -> ClusterSpec1D {
1223 let frames_len = rt_frames.frames.len();
1224 let scale = &*rt_frames.scale;
1225
1226 let (mut rt_lo, mut rt_hi) = rt.rt_bounds_frames;
1228 rt_lo = rt_lo
1229 .saturating_sub(opts.extra_rt_pad)
1230 .min(frames_len.saturating_sub(1));
1231 rt_hi = rt_hi
1232 .saturating_add(opts.extra_rt_pad)
1233 .min(frames_len.saturating_sub(1));
1234 if rt_lo > rt_hi {
1235 std::mem::swap(&mut rt_lo, &mut rt_hi);
1236 }
1237
1238 let mut im_lo = im.left.saturating_sub(opts.extra_im_pad);
1240 let mut im_hi = im.right.saturating_add(opts.extra_im_pad);
1241 if im_lo > im_hi {
1242 std::mem::swap(&mut im_lo, &mut im_hi);
1243 }
1244
1245 let prior_sigma = im.scan_sigma.unwrap_or(0.0).max(0.0);
1246 let k_im = if opts.im_k_sigma.is_finite() {
1247 opts.im_k_sigma.max(0.0)
1248 } else {
1249 3.0
1250 };
1251
1252 let want_from_prior = if prior_sigma > 0.0 {
1253 scans_from_sigma(k_im, prior_sigma)
1254 } else {
1255 0
1256 };
1257 let want_from_measured = im.width_scans.max(2);
1258
1259 let mut want_span = opts.min_im_span.max(want_from_measured);
1260 if want_from_prior > 0 {
1261 want_span = want_span.max(want_from_prior);
1262 }
1263
1264 let (w_lo, w_hi) = widen_scan_window(im_lo, im_hi, want_span, im.scan);
1265 im_lo = w_lo;
1266 im_hi = w_hi;
1267
1268 let axis_bounds = im.tof_bounds; let (mut bin_lo, mut bin_hi) = axis_bin_range_for_bounds(scale, axis_bounds);
1271 if bin_lo > bin_hi {
1272 std::mem::swap(&mut bin_lo, &mut bin_hi);
1273 }
1274
1275 let n_bins = scale.edges.len().saturating_sub(1);
1276 if n_bins > 0 {
1277 let pad = opts.tof_bin_pad;
1278 if pad > 0 {
1279 bin_lo = bin_lo.saturating_sub(pad);
1280 bin_hi = (bin_hi + pad).min(n_bins - 1);
1281 }
1282 } else {
1283 bin_lo = 0;
1284 bin_hi = 0;
1285 }
1286
1287 let tof_win = (bin_lo as i32, bin_hi as i32);
1288
1289 debug_assert!(
1290 bin_lo <= bin_hi && bin_hi < scale.num_bins(),
1291 "axis_bin_range_for_bounds produced invalid bin range: {:?} -> ({}, {}) / n_bins={}",
1292 im.tof_bounds, bin_lo, bin_hi, scale.num_bins()
1293 );
1294
1295 ClusterSpec1D {
1296 rt_lo,
1297 rt_hi,
1298 im_lo,
1299 im_hi,
1300 tof_win,
1301 tof_hist_bins: opts.tof_hist_bins.max(16),
1302 window_group: im.window_group,
1303 parent_im_id: Some(im.id),
1304 parent_rt_id: Some(rt.id),
1305 ms_level: opts.ms_level,
1306 im_prior_sigma: if prior_sigma > 0.0 { Some(prior_sigma) } else { None },
1307 }
1308}
1309
1310pub fn make_specs_from_im_and_rt_groups_threads(
1315 im_peaks: &[ImPeak1D],
1316 rt_groups: &[Vec<RtPeak1D>],
1317 rt_frames: &RtFrames,
1318 opts: &BuildSpecOpts,
1319 require_rt_overlap: bool,
1320 num_threads: usize,
1321) -> Vec<ClusterSpec1D> {
1322 assert_eq!(im_peaks.len(), rt_groups.len(), "rt_groups length mismatch");
1323 let n_frames = rt_frames.frames.len();
1324 if n_frames == 0 || im_peaks.is_empty() { return Vec::new(); }
1325
1326 let build = || {
1327 (0..im_peaks.len()).into_par_iter()
1328 .flat_map_iter(|i| {
1329 let im = &im_peaks[i];
1330 let rts = &rt_groups[i];
1331
1332 let mut out: Vec<ClusterSpec1D> = Vec::new();
1333 if rts.is_empty() { return out.into_iter(); }
1334
1335 let im_rt = rt_bounds_from_im(im, n_frames);
1336
1337 for rt in rts {
1338 let mut rt_lo = rt.rt_bounds_frames.0.min(n_frames.saturating_sub(1));
1339 let mut rt_hi = rt.rt_bounds_frames.1.min(n_frames.saturating_sub(1));
1340 if rt_lo > rt_hi { std::mem::swap(&mut rt_lo, &mut rt_hi); }
1341
1342 if require_rt_overlap && rt_overlap((rt_lo, rt_hi), im_rt) == 0 {
1343 continue;
1344 }
1345
1346 let mut spec = make_spec_from_pair(im, rt, rt_frames, opts);
1347 spec.rt_lo = spec.rt_lo.min(n_frames.saturating_sub(1));
1348 spec.rt_hi = spec.rt_hi.min(n_frames.saturating_sub(1));
1349 if spec.rt_lo > spec.rt_hi { std::mem::swap(&mut spec.rt_lo, &mut spec.rt_hi); }
1350 if spec.im_lo > spec.im_hi { std::mem::swap(&mut spec.im_lo, &mut spec.im_hi); }
1351
1352 out.push(spec);
1353 }
1354
1355 out.into_iter()
1356 })
1357 .collect::<Vec<_>>()
1358 };
1359
1360 if num_threads == 0 {
1361 build()
1362 } else {
1363 ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap().install(build)
1364 }
1365}
1366
1367#[inline]
1372fn cushion_hi_edge(scale: &TofScale, hi_edge: f32) -> f32 {
1373 let axis_max = scale
1376 .edges
1377 .last()
1378 .copied()
1379 .unwrap_or(hi_edge);
1380
1381 let n = scale.edges.len();
1382 let bin_width = if n >= 2 {
1383 let span = scale.edges[n - 1] - scale.edges[0];
1384 if span.is_finite() && span != 0.0 {
1385 (span / (n as f32 - 1.0)).abs()
1386 } else {
1387 0.0
1388 }
1389 } else {
1390 0.0
1391 };
1392
1393 let eps = 0.25 * bin_width; (hi_edge + eps).min(axis_max)
1395}
1396
1397#[inline]
1398fn thin_stride(total: usize, cap: usize) -> usize {
1399 if cap == 0 || total <= cap {
1400 1
1401 } else {
1402 (total + cap - 1) / cap
1403 }
1404}
1405
1406pub fn attach_raw_points_for_spec_1d_in_ctx(
1411 ctx: &RawAttachContext,
1412 scale: &TofScale,
1413 final_bin_lo: usize,
1414 final_bin_hi: usize,
1415 final_im_lo: usize,
1416 final_im_hi: usize,
1417 final_rt_lo: usize,
1418 final_rt_hi: usize,
1419 max_points: Option<usize>,
1420) -> RawPoints {
1421 let n_bins = scale.num_bins();
1422 if n_bins == 0 {
1423 return RawPoints::default();
1424 }
1425
1426 let mut bin_lo = final_bin_lo.min(n_bins.saturating_sub(1));
1428 let mut bin_hi = final_bin_hi.min(n_bins.saturating_sub(1));
1429 if bin_lo > bin_hi {
1430 std::mem::swap(&mut bin_lo, &mut bin_hi);
1431 }
1432
1433 let axis_lo = scale.edges[bin_lo];
1435 let hi_edge_idx = (bin_hi + 1).min(scale.edges.len().saturating_sub(1));
1436 let axis_hi = cushion_hi_edge(scale, scale.edges[hi_edge_idx]);
1437
1438 let slice = &ctx.slice;
1440 let scan_slices = &ctx.scan_slices;
1441
1442 let n_frames = slice.frames.len();
1444 if n_frames == 0 {
1445 return RawPoints::default();
1446 }
1447 let rt_lo = final_rt_lo.min(n_frames.saturating_sub(1));
1448 let rt_hi = final_rt_hi.min(n_frames.saturating_sub(1));
1449 if rt_lo > rt_hi {
1450 return RawPoints::default();
1451 }
1452
1453 let mut total = 0usize;
1455 for fi in rt_lo..=rt_hi {
1456 let fr = &slice.frames[fi];
1457 let tofs = &fr.tof; let len_all = tofs.len();
1459
1460 for sl in &scan_slices[fi] {
1461 let s_abs = sl.scan;
1462 if s_abs < final_im_lo || s_abs > final_im_hi {
1463 continue;
1464 }
1465
1466 let start = sl.start.min(len_all);
1468 let end = sl.end.min(len_all);
1469 for idx in start..end {
1470 let tof_val = tofs[idx] as f32;
1471 if tof_val >= axis_lo && tof_val < axis_hi {
1472 total += 1;
1473 }
1474 }
1475 }
1476 }
1477
1478 if total == 0 {
1480 return RawPoints::default();
1481 }
1482
1483 let stride = max_points
1484 .map(|cap| thin_stride(total, cap))
1485 .unwrap_or(1);
1486 let reserve = total / stride + 8;
1487
1488 let mut pts = RawPoints {
1489 mz: Vec::with_capacity(reserve),
1490 rt: Vec::with_capacity(reserve),
1491 im: Vec::with_capacity(reserve),
1492 scan: Vec::with_capacity(reserve),
1493 intensity: Vec::with_capacity(reserve),
1494 tof: Vec::with_capacity(reserve),
1495 frame: Vec::with_capacity(reserve),
1496 };
1497
1498 let rt_axis_sec = &ctx.rt_axis_sec[rt_lo..=rt_hi];
1499 let frame_ids_local = &ctx.frame_ids_local[rt_lo..=rt_hi];
1500
1501 let mut seen = 0usize;
1503 for fi in rt_lo..=rt_hi {
1504 let fr = &slice.frames[fi];
1505 let mz = &fr.ims_frame.mz;
1506 let it = &fr.ims_frame.intensity;
1507 let ims = &fr.ims_frame.mobility;
1508 let tofs = &fr.tof;
1509
1510 let len_all = mz
1511 .len()
1512 .min(it.len())
1513 .min(ims.len())
1514 .min(tofs.len());
1515
1516 let rt_val = rt_axis_sec[fi - rt_lo];
1517 let frame_id = frame_ids_local[fi - rt_lo];
1518
1519 for sl in &scan_slices[fi] {
1520 let s_abs = sl.scan;
1521 if s_abs < final_im_lo || s_abs > final_im_hi {
1522 continue;
1523 }
1524
1525 let start = sl.start.min(len_all);
1526 let end = sl.end.min(len_all);
1527 if start >= end {
1528 continue;
1529 }
1530
1531 let mut idx = start;
1532 while idx < end {
1533 let tof_val = tofs[idx] as f32;
1534 if tof_val >= axis_lo && tof_val < axis_hi {
1535 if stride == 1 || (seen % stride == 0) {
1536 pts.mz.push(mz[idx] as f32); pts.rt.push(rt_val);
1538 pts.im.push(ims[idx] as f32);
1539 pts.scan.push(s_abs as u32);
1540 pts.intensity.push(it[idx] as f32);
1541 pts.frame.push(frame_id);
1542 pts.tof.push(tofs[idx]); }
1544 seen += 1;
1545 }
1546 idx += 1;
1547 }
1548 }
1549 }
1550
1551 pts
1552}
1553
1554#[inline]
1555fn thin_f32_vec(v: &[f32], cap: Option<usize>) -> Vec<f32> {
1556 let n = v.len();
1557 let stride = cap.map(|c| thin_stride(n, c)).unwrap_or(1);
1558 if stride <= 1 {
1559 return v.to_vec();
1560 }
1561 let mut out = Vec::with_capacity((n + stride - 1) / stride);
1562 let mut i = 0usize;
1563 while i < n {
1564 out.push(v[i]);
1565 i += stride;
1566 }
1567 out
1568}
1569
1570pub fn extract_raw_points_for_clusters_in_ctx(
1575 ctx: &RawAttachContext,
1576 scale: &TofScale,
1577 clusters: &[ClusterResult1D],
1578 max_points: Option<usize>,
1579) -> Vec<RawPoints> {
1580 clusters
1581 .iter()
1582 .map(|c| {
1583 let (rt_lo, rt_hi) = c.rt_window;
1584 let (im_lo, im_hi) = c.im_window;
1585 let (tof_lo, tof_hi) = c.tof_window;
1586
1587 attach_raw_points_for_spec_1d_in_ctx(
1588 ctx,
1589 scale,
1590 tof_lo,
1591 tof_hi,
1592 im_lo,
1593 im_hi,
1594 rt_lo,
1595 rt_hi,
1596 max_points,
1597 )
1598 })
1599 .collect()
1600}
1601
1602#[derive(Clone, Debug)]
1603pub struct ClusterMergePolicy {
1604 pub require_same_ms_level: bool,
1605 pub require_same_window_group: bool,
1606 pub require_same_parents: bool,
1607
1608 pub keep_axes_if_identical: bool,
1611 pub keep_traces_if_identical: bool,
1612}
1613
1614impl Default for ClusterMergePolicy {
1615 fn default() -> Self {
1616 Self {
1617 require_same_ms_level: true,
1618 require_same_window_group: true,
1619 require_same_parents: true,
1620 keep_axes_if_identical: true,
1621 keep_traces_if_identical: true,
1622 }
1623 }
1624}
1625
1626#[derive(Clone, Debug)]
1627pub struct ClusterMergeDistancePolicy {
1628 pub max_rt_center_delta: f32,
1630 pub max_im_center_delta: f32,
1632 pub max_tof_center_delta: f32,
1634 pub max_mz_center_delta_da: f32,
1636}
1637
1638impl Default for ClusterMergeDistancePolicy {
1639 fn default() -> Self {
1640 Self {
1641 max_rt_center_delta: 0.0,
1642 max_im_center_delta: 0.0,
1643 max_tof_center_delta: 0.0,
1644 max_mz_center_delta_da: 0.0,
1645 }
1646 }
1647}
1648
1649fn sum_intensity(raw: &RawPoints) -> f32 {
1651 raw.intensity
1652 .iter()
1653 .copied()
1654 .fold(0.0_f32, |acc, x| acc + x)
1655}
1656
1657fn merge_raw_points(a: &Option<RawPoints>, b: &Option<RawPoints>) -> Option<RawPoints> {
1658 match (a, b) {
1659 (None, None) => None,
1660 (Some(x), None) => Some(x.clone()),
1661 (None, Some(y)) => Some(y.clone()),
1662 (Some(x), Some(y)) => {
1663 let total = x.mz.len() + y.mz.len();
1665
1666 let mut tmp = RawPoints {
1667 mz: Vec::with_capacity(total),
1668 rt: Vec::with_capacity(total),
1669 im: Vec::with_capacity(total),
1670 scan: Vec::with_capacity(total),
1671 intensity: Vec::with_capacity(total),
1672 tof: Vec::with_capacity(total),
1673 frame: Vec::with_capacity(total),
1674 };
1675
1676 tmp.mz.extend_from_slice(&x.mz);
1677 tmp.mz.extend_from_slice(&y.mz);
1678 tmp.rt.extend_from_slice(&x.rt);
1679 tmp.rt.extend_from_slice(&y.rt);
1680 tmp.im.extend_from_slice(&x.im);
1681 tmp.im.extend_from_slice(&y.im);
1682 tmp.scan.extend_from_slice(&x.scan);
1683 tmp.scan.extend_from_slice(&y.scan);
1684 tmp.intensity.extend_from_slice(&x.intensity);
1685 tmp.intensity.extend_from_slice(&y.intensity);
1686 tmp.tof.extend_from_slice(&x.tof);
1687 tmp.tof.extend_from_slice(&y.tof);
1688 tmp.frame.extend_from_slice(&x.frame);
1689 tmp.frame.extend_from_slice(&y.frame);
1690
1691 debug_assert_eq!(tmp.mz.len(), total);
1692 debug_assert_eq!(tmp.rt.len(), total);
1693 debug_assert_eq!(tmp.im.len(), total);
1694 debug_assert_eq!(tmp.scan.len(), total);
1695 debug_assert_eq!(tmp.intensity.len(), total);
1696 debug_assert_eq!(tmp.tof.len(), total);
1697 debug_assert_eq!(tmp.frame.len(), total);
1698
1699 use std::cmp::Ordering;
1701
1702 let n = total;
1703 let mut idx: Vec<usize> = (0..n).collect();
1704
1705 idx.sort_unstable_by(|&i, &j| {
1706 let fi = tmp.frame[i];
1707 let fj = tmp.frame[j];
1708 match fi.cmp(&fj) {
1709 Ordering::Less => return Ordering::Less,
1710 Ordering::Greater => return Ordering::Greater,
1711 Ordering::Equal => {}
1712 }
1713
1714 let si = tmp.scan[i];
1715 let sj = tmp.scan[j];
1716 match si.cmp(&sj) {
1717 Ordering::Less => return Ordering::Less,
1718 Ordering::Greater => return Ordering::Greater,
1719 Ordering::Equal => {}
1720 }
1721
1722 let ti = tmp.tof[i];
1723 let tj = tmp.tof[j];
1724 match ti.cmp(&tj) {
1725 Ordering::Less => return Ordering::Less,
1726 Ordering::Greater => return Ordering::Greater,
1727 Ordering::Equal => {}
1728 }
1729
1730 let mi = tmp.mz[i].to_bits();
1732 let mj = tmp.mz[j].to_bits();
1733 match mi.cmp(&mj) {
1734 Ordering::Less => return Ordering::Less,
1735 Ordering::Greater => return Ordering::Greater,
1736 Ordering::Equal => {}
1737 }
1738
1739 i.cmp(&j)
1741 });
1742
1743 let mut out = RawPoints {
1746 mz: Vec::with_capacity(n),
1747 rt: Vec::with_capacity(n),
1748 im: Vec::with_capacity(n),
1749 scan: Vec::with_capacity(n),
1750 intensity: Vec::with_capacity(n),
1751 tof: Vec::with_capacity(n),
1752 frame: Vec::with_capacity(n),
1753 };
1754
1755 let mut last_key: Option<(u32, u32, i32, u32)> = None;
1756
1757 for k in idx {
1758 let key = (
1759 tmp.frame[k],
1760 tmp.scan[k],
1761 tmp.tof[k],
1762 tmp.mz[k].to_bits(),
1763 );
1764
1765 if Some(key) == last_key {
1766 continue;
1768 }
1769 last_key = Some(key);
1770
1771 out.mz.push(tmp.mz[k]);
1772 out.rt.push(tmp.rt[k]);
1773 out.im.push(tmp.im[k]);
1774 out.scan.push(tmp.scan[k]);
1775 out.intensity.push(tmp.intensity[k]);
1776 out.tof.push(tmp.tof[k]);
1777 out.frame.push(tmp.frame[k]);
1778 }
1779
1780 Some(out)
1781 }
1782 }
1783}
1784
1785fn merge_fit1d(a: &Fit1D, b: &Fit1D) -> Fit1D {
1786 if a.area <= 0.0 {
1788 return b.clone();
1789 }
1790 if b.area <= 0.0 {
1791 return a.clone();
1792 }
1793
1794 let a_area = a.area;
1795 let b_area = b.area;
1796 let total = a_area + b_area;
1797
1798 let mu = (a.mu * a_area + b.mu * b_area) / total;
1799
1800 let s1 = a.sigma * a.sigma + a.mu * a.mu;
1801 let s2 = b.sigma * b.sigma + b.mu * b.mu;
1802 let second_moment = (a_area * s1 + b_area * s2) / total;
1803 let var = (second_moment - mu * mu).max(0.0);
1804 let sigma = var.sqrt();
1805
1806 let height = a.height.max(b.height);
1808
1809 let mut out = a.clone();
1810 out.mu = mu;
1811 out.sigma = sigma;
1812 out.area = total;
1813 out.height = height;
1814 out
1815}
1816
1817fn merge_opt_fit1d(a: &Option<Fit1D>, b: &Option<Fit1D>) -> Option<Fit1D> {
1818 match (a, b) {
1819 (None, None) => None,
1820 (Some(x), None) => Some(x.clone()),
1821 (None, Some(y)) => Some(y.clone()),
1822 (Some(x), Some(y)) => Some(merge_fit1d(x, y)),
1823 }
1824}
1825
1826fn merge_axis_vec<T: Clone + PartialEq>(
1827 a: &Option<Vec<T>>,
1828 b: &Option<Vec<T>>,
1829 keep_if_identical: bool,
1830) -> Option<Vec<T>> {
1831 match (a, b) {
1832 (None, None) => None,
1833 (Some(x), None) => Some(x.clone()),
1834 (None, Some(y)) => Some(y.clone()),
1835 (Some(x), Some(y)) => {
1836 if keep_if_identical && x == y {
1837 Some(x.clone())
1838 } else if keep_if_identical {
1839 None
1841 } else {
1842 None
1844 }
1845 }
1846 }
1847}
1848
1849fn merge_trace_vec(
1850 a: &Option<Vec<f32>>,
1851 b: &Option<Vec<f32>>,
1852 keep_if_identical: bool,
1853) -> Option<Vec<f32>> {
1854 match (a, b) {
1855 (None, None) => None,
1856 (Some(x), None) => Some(x.clone()),
1857 (None, Some(y)) => Some(y.clone()),
1858 (Some(x), Some(y)) => {
1859 if keep_if_identical && x == y {
1860 Some(x.clone())
1861 } else if keep_if_identical {
1862 None
1863 } else {
1864 None
1865 }
1866 }
1867 }
1868}
1869
1870pub fn clusters_can_merge(
1871 a: &ClusterResult1D,
1872 b: &ClusterResult1D,
1873 policy: &ClusterMergePolicy,
1874) -> bool {
1875 if policy.require_same_ms_level && a.ms_level != b.ms_level {
1876 return false;
1877 }
1878 if policy.require_same_window_group && a.window_group != b.window_group {
1879 return false;
1880 }
1881 if policy.require_same_parents {
1882 if a.parent_im_id != b.parent_im_id || a.parent_rt_id != b.parent_rt_id {
1883 return false;
1884 }
1885 }
1886
1887 let rt_overlaps = a.rt_window.1 >= b.rt_window.0 && b.rt_window.1 >= a.rt_window.0;
1889 let im_overlaps = a.im_window.1 >= b.im_window.0 && b.im_window.1 >= a.im_window.0;
1890 let tof_overlaps = a.tof_window.1 >= b.tof_window.0 && b.tof_window.1 >= a.tof_window.0;
1891
1892 rt_overlaps && im_overlaps && tof_overlaps
1893}
1894
1895pub fn merge_clusters(
1896 a: &ClusterResult1D,
1897 b: &ClusterResult1D,
1898 new_id: u64,
1899 policy: &ClusterMergePolicy,
1900) -> ClusterResult1D {
1901 debug_assert!(
1902 clusters_can_merge(a, b, policy),
1903 "merge_clusters called on incompatible clusters"
1904 );
1905
1906 let rt_window = (
1907 a.rt_window.0.min(b.rt_window.0),
1908 a.rt_window.1.max(b.rt_window.1),
1909 );
1910 let im_window = (
1911 a.im_window.0.min(b.im_window.0),
1912 a.im_window.1.max(b.im_window.1),
1913 );
1914 let tof_window = (
1915 a.tof_window.0.min(b.tof_window.0),
1916 a.tof_window.1.max(b.tof_window.1),
1917 );
1918 let tof_index_window = (
1919 a.tof_index_window.0.min(b.tof_index_window.0),
1920 a.tof_index_window.1.max(b.tof_index_window.1),
1921 );
1922
1923 let rt_fit = merge_fit1d(&a.rt_fit, &b.rt_fit);
1924 let im_fit = merge_fit1d(&a.im_fit, &b.im_fit);
1925 let tof_fit = merge_fit1d(&a.tof_fit, &b.tof_fit);
1926 let mz_fit = merge_opt_fit1d(&a.mz_fit, &b.mz_fit);
1927
1928 let raw_points = merge_raw_points(&a.raw_points, &b.raw_points);
1930
1931 let (raw_sum, volume_proxy) = if a.raw_points.is_some() || b.raw_points.is_some() {
1939 if let Some(ref rp) = raw_points {
1940 let s = sum_intensity(rp);
1941 (s, s)
1944 } else {
1945 if a.raw_sum >= b.raw_sum {
1948 (a.raw_sum, a.volume_proxy)
1949 } else {
1950 (b.raw_sum, b.volume_proxy)
1951 }
1952 }
1953 } else {
1954 if a.raw_sum >= b.raw_sum {
1956 (a.raw_sum, a.volume_proxy)
1957 } else {
1958 (b.raw_sum, b.volume_proxy)
1959 }
1960 };
1961
1962 let mut frame_ids_used = a.frame_ids_used.clone();
1964 frame_ids_used.extend_from_slice(&b.frame_ids_used);
1965 frame_ids_used.sort_unstable();
1966 frame_ids_used.dedup();
1967
1968 let window_group = a.window_group.or(b.window_group);
1969 let parent_im_id = a.parent_im_id.or(b.parent_im_id);
1970 let parent_rt_id = a.parent_rt_id.or(b.parent_rt_id);
1971 let ms_level = a.ms_level; let rt_axis_sec =
1974 merge_axis_vec(&a.rt_axis_sec, &b.rt_axis_sec, policy.keep_axes_if_identical);
1975 let im_axis_scans =
1976 merge_axis_vec(&a.im_axis_scans, &b.im_axis_scans, policy.keep_axes_if_identical);
1977 let mz_axis_da =
1978 merge_axis_vec(&a.mz_axis_da, &b.mz_axis_da, policy.keep_axes_if_identical);
1979
1980 let rt_trace = merge_trace_vec(&a.rt_trace, &b.rt_trace, policy.keep_traces_if_identical);
1981 let im_trace = merge_trace_vec(&a.im_trace, &b.im_trace, policy.keep_traces_if_identical);
1982
1983 ClusterResult1D {
1984 cluster_id: new_id,
1985 rt_window,
1986 im_window,
1987 tof_window,
1988 tof_index_window,
1989 mz_window: a.mz_window.or(b.mz_window),
1990
1991 rt_fit,
1992 im_fit,
1993 tof_fit,
1994 mz_fit,
1995
1996 raw_sum,
1997 volume_proxy,
1998
1999 frame_ids_used,
2000 window_group,
2001 parent_im_id,
2002 parent_rt_id,
2003 ms_level,
2004
2005 rt_axis_sec,
2006 im_axis_scans,
2007 mz_axis_da,
2008
2009 raw_points,
2010 rt_trace,
2011 im_trace,
2012 }
2013}
2014
2015pub fn merge_cluster_group(
2016 clusters: &[ClusterResult1D],
2017 new_id: u64,
2018 policy: &ClusterMergePolicy,
2019) -> Option<ClusterResult1D> {
2020 let mut it = clusters.iter();
2021 let first = it.next()?.clone();
2022 Some(it.fold(first, |acc, c| merge_clusters(&acc, c, new_id, policy)))
2023}
2024
2025fn clusters_can_merge_with_distance(
2028 a: &ClusterResult1D,
2029 b: &ClusterResult1D,
2030 dist: &ClusterMergeDistancePolicy,
2031) -> bool {
2032 if a.ms_level != b.ms_level {
2034 return false;
2035 }
2036 if a.window_group != b.window_group {
2037 return false;
2038 }
2039
2040 let rt_overlaps = a.rt_window.1 >= b.rt_window.0 && b.rt_window.1 >= a.rt_window.0;
2042 let im_overlaps = a.im_window.1 >= b.im_window.0 && b.im_window.1 >= a.im_window.0;
2043 let tof_overlaps = a.tof_window.1 >= b.tof_window.0 && b.tof_window.1 >= a.tof_window.0;
2044 if !(rt_overlaps && im_overlaps && tof_overlaps) {
2045 return false;
2046 }
2047
2048 let drt = (a.rt_fit.mu - b.rt_fit.mu).abs();
2050 if dist.max_rt_center_delta > 0.0 && drt > dist.max_rt_center_delta {
2051 return false;
2052 }
2053
2054 let dim = (a.im_fit.mu - b.im_fit.mu).abs();
2055 if dist.max_im_center_delta > 0.0 && dim > dist.max_im_center_delta {
2056 return false;
2057 }
2058
2059 let dtof = (a.tof_fit.mu - b.tof_fit.mu).abs();
2060 if dist.max_tof_center_delta > 0.0 && dtof > dist.max_tof_center_delta {
2061 return false;
2062 }
2063
2064 if dist.max_mz_center_delta_da > 0.0 {
2066 if let (Some(mza), Some(mzb)) = (&a.mz_fit, &b.mz_fit) {
2067 let dmz = (mza.mu - mzb.mu).abs();
2068 if dmz > dist.max_mz_center_delta_da {
2069 return false;
2070 }
2071 }
2072 }
2073
2074 true
2075}
2076
2077pub fn merge_clusters_by_distance(
2083 mut clusters: Vec<ClusterResult1D>,
2084 dist: &ClusterMergeDistancePolicy,
2085) -> Vec<ClusterResult1D> {
2086 if clusters.len() <= 1 {
2087 return clusters;
2088 }
2089
2090 clusters.sort_unstable_by(|a, b| {
2092 a.ms_level
2093 .cmp(&b.ms_level)
2094 .then_with(|| a.window_group.cmp(&b.window_group))
2095 .then_with(|| {
2096 a.tof_fit
2097 .mu
2098 .partial_cmp(&b.tof_fit.mu)
2099 .unwrap_or(std::cmp::Ordering::Equal)
2100 })
2101 .then_with(|| {
2102 a.rt_fit
2103 .mu
2104 .partial_cmp(&b.rt_fit.mu)
2105 .unwrap_or(std::cmp::Ordering::Equal)
2106 })
2107 .then_with(|| {
2108 a.im_fit
2109 .mu
2110 .partial_cmp(&b.im_fit.mu)
2111 .unwrap_or(std::cmp::Ordering::Equal)
2112 })
2113 .then_with(|| a.cluster_id.cmp(&b.cluster_id))
2114 });
2115
2116 let merge_policy = ClusterMergePolicy {
2119 require_same_ms_level: false,
2120 require_same_window_group: false,
2121 require_same_parents: false,
2122 keep_axes_if_identical: true,
2123 keep_traces_if_identical: true,
2124 };
2125
2126 let mut out: Vec<ClusterResult1D> = Vec::new();
2127 let mut group: Vec<ClusterResult1D> = Vec::new();
2128
2129 for c in clusters.into_iter() {
2130 if let Some(last) = group.last() {
2131 if clusters_can_merge_with_distance(last, &c, dist) {
2132 group.push(c);
2134 } else {
2135 let new_id = group[0].cluster_id; if let Some(merged) = merge_cluster_group(&group, new_id, &merge_policy) {
2138 out.push(merged);
2139 }
2140 group.clear();
2141 group.push(c);
2142 }
2143 } else {
2144 group.push(c);
2145 }
2146 }
2147
2148 if !group.is_empty() {
2150 let new_id = group[0].cluster_id;
2151 if let Some(merged) = merge_cluster_group(&group, new_id, &merge_policy) {
2152 out.push(merged);
2153 }
2154 }
2155
2156 out
2157}