Skip to main content

flow_pacmap/
pairs.rs

1//! Near, mid-near, and further pair construction from Algorithm 1.
2//!
3//! All pair indices are `u32`; the entry guard in `lib.rs` ensures n ≤ u32::MAX.
4//! All multiplications use `checked_mul` before any allocation.
5
6use crate::error::PaCMAPError;
7use crate::knn::NeighborList;
8use rand::{RngExt, SeedableRng, rngs::SmallRng};
9use rayon::prelude::*;
10
11/// Pre-allocated pair storage for all three pair types.
12pub struct Pairs {
13    /// Near pairs: (point_i, near_neighbour_j) — shape [n * n_nb, 2]
14    pub near: Vec<[u32; 2]>,
15    /// Mid-near pairs — shape [n * n_mn, 2]
16    pub mid_near: Vec<[u32; 2]>,
17    /// Further pairs — shape [n * n_fp, 2]
18    pub further: Vec<[u32; 2]>,
19}
20
21/// Build all three pair types from pre-computed KNN results.
22///
23/// # Overflow safety
24/// All `n * k` multiplications use `checked_mul`. `n` is guaranteed ≤ u32::MAX
25/// by the entry guard in `lib.rs`.
26pub fn build_pairs(
27    knn: &[NeighborList],
28    data: &[f32],
29    n: usize,
30    d: usize,
31    n_nb: usize,
32    n_mn: usize,
33    n_fp: usize,
34    seed: Option<u64>,
35) -> Result<Pairs, PaCMAPError> {
36    let cap_nb = n
37        .checked_mul(n_nb)
38        .ok_or(PaCMAPError::PairCountOverflow { n, k: n_nb })?;
39    let cap_mn = n
40        .checked_mul(n_mn)
41        .ok_or(PaCMAPError::PairCountOverflow { n, k: n_mn })?;
42    let cap_fp = n
43        .checked_mul(n_fp)
44        .ok_or(PaCMAPError::PairCountOverflow { n, k: n_fp })?;
45
46    // ── Near pairs ─────────────────────────────────────────────────────────
47    // For each i, compute scaled distance d²_select = ‖xi−xj‖² / (σi·σj)
48    // and keep the top n_nb by scaled distance from the candidate set.
49
50    // σi = average distance to 4th–6th Euclidean neighbours
51    let sigma: Vec<f32> = knn
52        .par_iter()
53        .map(|nl| {
54            let start = 3.min(nl.distances.len());
55            let end = 6.min(nl.distances.len());
56            if start >= end {
57                // Fallback for very small n: use available distances
58                if nl.distances.is_empty() {
59                    1.0
60                } else {
61                    nl.distances.iter().sum::<f32>() / nl.distances.len() as f32
62                }
63            } else {
64                nl.distances[start..end].iter().sum::<f32>() / (end - start) as f32
65            }
66        })
67        .collect();
68
69    let mut near: Vec<[u32; 2]> = Vec::with_capacity(cap_nb);
70
71    // Build near pairs sequentially per point (scaled distance reranking is cheap)
72    for (i, nl) in knn.iter().enumerate() {
73        let row_i = &data[i * d..(i + 1) * d];
74        let sigma_i = sigma[i].max(f32::EPSILON);
75
76        // Compute scaled distance for each candidate
77        let mut scaled: Vec<(f32, u32)> = nl
78            .indices
79            .iter()
80            .map(|&j| {
81                let row_j = &data[j as usize * d..(j as usize + 1) * d];
82                let l2sq: f32 = row_i
83                    .iter()
84                    .zip(row_j)
85                    .map(|(a, b)| (a - b) * (a - b))
86                    .sum();
87                let sigma_j = sigma[j as usize].max(f32::EPSILON);
88                let d_scaled = l2sq / (sigma_i * sigma_j);
89                (d_scaled, j)
90            })
91            .collect();
92
93        // Keep top n_nb by scaled distance
94        scaled.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
95        for (_, j) in scaled.iter().take(n_nb) {
96            near.push([i as u32, *j]);
97        }
98    }
99
100    // ── Mid-near pairs ──────────────────────────────────────────────────────
101    // For each i: sample 6 random points, use the 2nd closest as mid-near partner.
102    // Repeat n_mn times.
103    let base_seed = seed.unwrap_or(42);
104
105    let mut mid_near: Vec<[u32; 2]> = Vec::with_capacity(cap_mn);
106    // Sequential per-point to avoid RNG sharing across threads
107    for i in 0..n {
108        let row_i = &data[i * d..(i + 1) * d];
109        let mut rng = SmallRng::seed_from_u64(base_seed.wrapping_add(i as u64));
110        for _ in 0..n_mn {
111            let candidates = sample_6_excluding(&mut rng, n as u32, i as u32);
112            // Find 2nd closest candidate (index 1 in sorted order)
113            let mut dists: Vec<(f32, u32)> = candidates
114                .iter()
115                .map(|&j| {
116                    let row_j = &data[j as usize * d..(j as usize + 1) * d];
117                    let d2: f32 = row_i
118                        .iter()
119                        .zip(row_j)
120                        .map(|(a, b)| (a - b) * (a - b))
121                        .sum();
122                    (d2, j)
123                })
124                .collect();
125            dists.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
126            if let Some(second) = dists.get(1) {
127                mid_near.push([i as u32, second.1]);
128            } else if let Some(first) = dists.first() {
129                mid_near.push([i as u32, first.1]);
130            }
131        }
132    }
133
134    // ── Further pairs ───────────────────────────────────────────────────────
135    // For each i: sample n_fp random non-neighbour points.
136    // Build a set of near-neighbour indices per point for rejection sampling.
137    let mut further: Vec<[u32; 2]> = Vec::with_capacity(cap_fp);
138    for (i, _) in knn.iter().enumerate().take(n) {
139        let mut rng = SmallRng::seed_from_u64(
140            base_seed
141                .wrapping_add(i as u64)
142                .wrapping_add(0xdeadbeef_cafebabe),
143        );
144        let near_set: std::collections::HashSet<u32> = knn[i].indices.iter().copied().collect();
145        let mut count = 0usize;
146        let mut attempts = 0usize;
147        while count < n_fp && attempts < n_fp * 100 {
148            let j = rng.random_range(0..n as u32);
149            if j != i as u32 && !near_set.contains(&j) {
150                further.push([i as u32, j]);
151                count += 1;
152            }
153            attempts += 1;
154        }
155        // If rejection sampling exhausted, pad with whatever we have
156        // (only happens for pathologically small n)
157        if count < n_fp {
158            for j in 0..n as u32 {
159                if j != i as u32 && !near_set.contains(&j) && count < n_fp {
160                    further.push([i as u32, j]);
161                    count += 1;
162                }
163            }
164        }
165    }
166
167    Ok(Pairs {
168        near,
169        mid_near,
170        further,
171    })
172}
173
174/// Sample 6 distinct random indices in [0, max) excluding `exclude`.
175fn sample_6_excluding(rng: &mut SmallRng, max: u32, exclude: u32) -> Vec<u32> {
176    let mut result = Vec::with_capacity(6);
177    let mut attempts = 0u32;
178    while result.len() < 6 && attempts < 1000 {
179        let j = rng.random_range(0..max);
180        if j != exclude && !result.contains(&j) {
181            result.push(j);
182        }
183        attempts += 1;
184    }
185    result
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::knn::NeighborList;
192
193    fn make_knn(n: usize, k: usize) -> Vec<NeighborList> {
194        (0..n)
195            .map(|i| NeighborList {
196                indices: (0..k).map(|t| ((i + t + 1) % n) as u32).collect(),
197                distances: vec![1.0; k],
198            })
199            .collect()
200    }
201
202    #[test]
203    fn pair_counts_match_expected() {
204        let n = 20;
205        let n_nb = 5;
206        let n_mn = 2;
207        let n_fp = 4;
208        let data: Vec<f32> = (0..n * 3).map(|i| i as f32).collect();
209        let knn = make_knn(n, 10);
210        let pairs = build_pairs(&knn, &data, n, 3, n_nb, n_mn, n_fp, Some(0)).unwrap();
211        assert_eq!(pairs.near.len(), n * n_nb);
212        assert_eq!(pairs.mid_near.len(), n * n_mn);
213        assert_eq!(pairs.further.len(), n * n_fp);
214    }
215
216    #[test]
217    fn near_pairs_no_self_loops() {
218        let n = 20;
219        let data: Vec<f32> = (0..n * 3).map(|i| i as f32).collect();
220        let knn = make_knn(n, 10);
221        let pairs = build_pairs(&knn, &data, n, 3, 5, 2, 4, Some(0)).unwrap();
222        for p in &pairs.near {
223            assert_ne!(p[0], p[1], "near pair should not be a self-loop");
224        }
225    }
226}