1use crate::error::PaCMAPError;
7use crate::knn::NeighborList;
8use rand::{RngExt, SeedableRng, rngs::SmallRng};
9use rayon::prelude::*;
10
11pub struct Pairs {
13 pub near: Vec<[u32; 2]>,
15 pub mid_near: Vec<[u32; 2]>,
17 pub further: Vec<[u32; 2]>,
19}
20
21pub 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 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 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 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 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 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 let base_seed = seed.unwrap_or(42);
104
105 let mut mid_near: Vec<[u32; 2]> = Vec::with_capacity(cap_mn);
106 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 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 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 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
174fn 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}