use crate::params::WatershedParams;
use std::collections::HashMap;
use std::hash::{BuildHasher, Hasher};
#[derive(Default, Clone, Copy)]
pub(crate) struct U64Hasher(u64);
impl Hasher for U64Hasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, _: &[u8]) {
unreachable!("U64Hasher is only used with u64 keys")
}
fn write_u64(&mut self, n: u64) {
let mut x = n;
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^= x >> 31;
self.0 = x;
}
}
#[derive(Default, Clone, Copy)]
pub(crate) struct BuildU64Hasher;
impl BuildHasher for BuildU64Hasher {
type Hasher = U64Hasher;
fn build_hasher(&self) -> U64Hasher {
U64Hasher(0)
}
}
pub fn watershed_centroid(
points: &[(u32, u32, u32)],
p: &WatershedParams,
max_centroids: usize,
) -> Vec<(u32, u32, u32)> {
let n = points.len();
if n == 0 {
return Vec::new();
}
let cell_w_scan = p.box_scan.max(1);
let cell_w_tof = p.box_mz_idx.max(1);
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_by(|&a, &b| {
let (sa, ta, ia) = points[a as usize];
let (sb, tb, ib) = points[b as usize];
ib.cmp(&ia).then(sa.cmp(&sb)).then(ta.cmp(&tb))
});
let mut grid: HashMap<u64, Vec<u32>, BuildU64Hasher> = HashMap::with_hasher(BuildU64Hasher);
let mut group_id: Vec<i32> = vec![-1; n];
let mut seed_intensities: Vec<u32> = Vec::new();
let mut seed_tofs: Vec<u32> = Vec::new();
let key = |cs: u32, ct: u32| -> u64 { ((cs as u64) << 32) | (ct as u64) };
for &p_idx in &order {
if seed_intensities.len() >= max_centroids {
break;
}
let pi = p_idx as usize;
let (ps, pt, p_int) = points[pi];
let cell_s = ps / cell_w_scan;
let cell_t = pt / cell_w_tof;
let mut best_dist: u32 = u32::MAX;
let mut best_q: i32 = -1;
let mut best_seed: u32 = 0;
let mut found = false;
for ds in -1i64..=1 {
for dt in -1i64..=1 {
let cs = match (cell_s as i64).checked_add(ds) {
Some(v) if v >= 0 => v as u32,
_ => continue,
};
let ct = match (cell_t as i64).checked_add(dt) {
Some(v) if v >= 0 => v as u32,
_ => continue,
};
if let Some(bucket) = grid.get(&key(cs, ct)) {
for &q_idx in bucket {
let qi = q_idx as usize;
let (qs, qt, _) = points[qi];
let ds_abs = ps.abs_diff(qs);
let dt_abs = pt.abs_diff(qt);
if ds_abs > p.box_scan || dt_abs > p.box_mz_idx {
continue;
}
let gid = group_id[qi] as usize;
if pt.abs_diff(seed_tofs[gid]) > p.max_tof_offset {
continue;
}
let dist = ds_abs + dt_abs;
let q_seed = seed_intensities[gid];
if !found || dist < best_dist || (dist == best_dist && q_seed > best_seed) {
found = true;
best_dist = dist;
best_q = q_idx as i32;
best_seed = q_seed;
}
}
}
}
}
if best_q >= 0 {
let g = group_id[best_q as usize];
group_id[pi] = g;
grid.entry(key(cell_s, cell_t)).or_default().push(p_idx);
} else if p_int as u64 >= p.min_seed_intensity {
let g = seed_intensities.len() as i32;
group_id[pi] = g;
seed_intensities.push(p_int);
seed_tofs.push(pt);
grid.entry(key(cell_s, cell_t)).or_default().push(p_idx);
}
}
let n_groups = seed_intensities.len();
if n_groups == 0 {
return Vec::new();
}
let mut sum_int = vec![0u64; n_groups];
let mut sum_scan_w = vec![0.0f64; n_groups];
let mut sum_tof_w = vec![0.0f64; n_groups];
for i in 0..n {
let g = group_id[i];
if g < 0 {
continue;
}
let g = g as usize;
let (s, t, int) = points[i];
let w = int as f64;
sum_int[g] += int as u64;
sum_scan_w[g] += s as f64 * w;
sum_tof_w[g] += t as f64 * w;
}
let mut merged: HashMap<u64, u64, BuildU64Hasher> = HashMap::with_hasher(BuildU64Hasher);
let mut keys_in_order: Vec<u64> = Vec::with_capacity(n_groups);
for g in 0..n_groups {
if sum_int[g] < p.min_centroid_total {
continue;
}
let total = sum_int[g] as f64;
let scan = (sum_scan_w[g] / total).round() as u32;
let tof = (sum_tof_w[g] / total).round() as u32;
let k = ((scan as u64) << 32) | (tof as u64);
let slot = merged.entry(k);
if let std::collections::hash_map::Entry::Vacant(_) = slot {
keys_in_order.push(k);
}
*merged.entry(k).or_insert(0) += sum_int[g];
}
keys_in_order
.into_iter()
.map(|k| {
let scan = (k >> 32) as u32;
let tof = (k & 0xFFFF_FFFF) as u32;
let intensity = merged[&k].min(u32::MAX as u64) as u32;
(scan, tof, intensity)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn params() -> WatershedParams {
WatershedParams {
box_scan: 5,
box_mz_idx: 3,
min_seed_intensity: 0,
min_centroid_total: 0,
max_tof_offset: 10,
}
}
fn n_out(points: &[(u32, u32, u32)], p: &WatershedParams) -> usize {
watershed_centroid(points, p, usize::MAX).len()
}
#[test]
fn empty_in_empty_out() {
assert_eq!(watershed_centroid(&[], ¶ms(), usize::MAX), Vec::new());
}
#[test]
fn two_separated_peaks_emit_two_centroids() {
let pts = vec![
(10, 100, 10),
(10, 101, 5),
(11, 100, 5),
(40, 200, 9),
(40, 201, 4),
(41, 200, 4),
];
assert_eq!(n_out(&pts, ¶ms()), 2);
}
#[test]
fn single_ridge_merges_via_followers() {
let pts = vec![
(10, 100, 4),
(11, 100, 8),
(12, 100, 10),
(13, 100, 8),
(14, 100, 4),
];
let p = WatershedParams {
box_scan: 2,
box_mz_idx: 1,
..params()
};
let out = watershed_centroid(&pts, &p, usize::MAX);
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, 12);
assert_eq!(out[0].1, 100);
assert_eq!(out[0].2, 4 + 8 + 10 + 8 + 4);
}
#[test]
fn orphan_dropped_below_seed_floor() {
let pts = vec![(10, 100, 1)];
let p = WatershedParams {
min_seed_intensity: 5,
..params()
};
assert_eq!(n_out(&pts, &p), 0);
}
#[test]
fn max_tof_offset_caps_follower_chain() {
let pts = vec![
(10, 100, 10),
(10, 102, 9),
(10, 104, 8),
(10, 106, 7),
(10, 108, 6),
(10, 110, 5),
];
let loose = WatershedParams {
box_scan: 1,
box_mz_idx: 2,
max_tof_offset: 100,
..params()
};
assert_eq!(n_out(&pts, &loose), 1);
let tight = WatershedParams {
box_scan: 1,
box_mz_idx: 2,
max_tof_offset: 4,
..params()
};
assert!(n_out(&pts, &tight) > 1);
}
#[test]
fn min_centroid_total_drops_weak_group() {
let pts = vec![(10, 100, 3)];
let p = WatershedParams {
min_centroid_total: 5,
..params()
};
assert_eq!(n_out(&pts, &p), 0);
}
#[test]
fn colliding_groups_are_merged() {
let pts = vec![(10, 100, 10), (10, 100, 20)];
let out = watershed_centroid(&pts, ¶ms(), usize::MAX);
assert_eq!(out.len(), 1);
assert_eq!(out[0], (10, 100, 30));
}
}