use crate::error::PaCMAPError;
use crate::knn::NeighborList;
use rand::{RngExt, SeedableRng, rngs::SmallRng};
use rayon::prelude::*;
pub struct Pairs {
pub near: Vec<[u32; 2]>,
pub mid_near: Vec<[u32; 2]>,
pub further: Vec<[u32; 2]>,
}
pub fn build_pairs(
knn: &[NeighborList],
data: &[f32],
n: usize,
d: usize,
n_nb: usize,
n_mn: usize,
n_fp: usize,
seed: Option<u64>,
) -> Result<Pairs, PaCMAPError> {
let cap_nb = n
.checked_mul(n_nb)
.ok_or(PaCMAPError::PairCountOverflow { n, k: n_nb })?;
let cap_mn = n
.checked_mul(n_mn)
.ok_or(PaCMAPError::PairCountOverflow { n, k: n_mn })?;
let cap_fp = n
.checked_mul(n_fp)
.ok_or(PaCMAPError::PairCountOverflow { n, k: n_fp })?;
let sigma: Vec<f32> = knn
.par_iter()
.map(|nl| {
let start = 3.min(nl.distances.len());
let end = 6.min(nl.distances.len());
if start >= end {
if nl.distances.is_empty() {
1.0
} else {
nl.distances.iter().sum::<f32>() / nl.distances.len() as f32
}
} else {
nl.distances[start..end].iter().sum::<f32>() / (end - start) as f32
}
})
.collect();
let mut near: Vec<[u32; 2]> = Vec::with_capacity(cap_nb);
for (i, nl) in knn.iter().enumerate() {
let row_i = &data[i * d..(i + 1) * d];
let sigma_i = sigma[i].max(f32::EPSILON);
let mut scaled: Vec<(f32, u32)> = nl
.indices
.iter()
.map(|&j| {
let row_j = &data[j as usize * d..(j as usize + 1) * d];
let l2sq: f32 = row_i
.iter()
.zip(row_j)
.map(|(a, b)| (a - b) * (a - b))
.sum();
let sigma_j = sigma[j as usize].max(f32::EPSILON);
let d_scaled = l2sq / (sigma_i * sigma_j);
(d_scaled, j)
})
.collect();
scaled.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
for (_, j) in scaled.iter().take(n_nb) {
near.push([i as u32, *j]);
}
}
let base_seed = seed.unwrap_or(42);
let mut mid_near: Vec<[u32; 2]> = Vec::with_capacity(cap_mn);
for i in 0..n {
let row_i = &data[i * d..(i + 1) * d];
let mut rng = SmallRng::seed_from_u64(base_seed.wrapping_add(i as u64));
for _ in 0..n_mn {
let candidates = sample_6_excluding(&mut rng, n as u32, i as u32);
let mut dists: Vec<(f32, u32)> = candidates
.iter()
.map(|&j| {
let row_j = &data[j as usize * d..(j as usize + 1) * d];
let d2: f32 = row_i
.iter()
.zip(row_j)
.map(|(a, b)| (a - b) * (a - b))
.sum();
(d2, j)
})
.collect();
dists.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
if let Some(second) = dists.get(1) {
mid_near.push([i as u32, second.1]);
} else if let Some(first) = dists.first() {
mid_near.push([i as u32, first.1]);
}
}
}
let mut further: Vec<[u32; 2]> = Vec::with_capacity(cap_fp);
for (i, _) in knn.iter().enumerate().take(n) {
let mut rng = SmallRng::seed_from_u64(
base_seed
.wrapping_add(i as u64)
.wrapping_add(0xdeadbeef_cafebabe),
);
let near_set: std::collections::HashSet<u32> = knn[i].indices.iter().copied().collect();
let mut count = 0usize;
let mut attempts = 0usize;
while count < n_fp && attempts < n_fp * 100 {
let j = rng.random_range(0..n as u32);
if j != i as u32 && !near_set.contains(&j) {
further.push([i as u32, j]);
count += 1;
}
attempts += 1;
}
if count < n_fp {
for j in 0..n as u32 {
if j != i as u32 && !near_set.contains(&j) && count < n_fp {
further.push([i as u32, j]);
count += 1;
}
}
}
}
Ok(Pairs {
near,
mid_near,
further,
})
}
fn sample_6_excluding(rng: &mut SmallRng, max: u32, exclude: u32) -> Vec<u32> {
let mut result = Vec::with_capacity(6);
let mut attempts = 0u32;
while result.len() < 6 && attempts < 1000 {
let j = rng.random_range(0..max);
if j != exclude && !result.contains(&j) {
result.push(j);
}
attempts += 1;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::knn::NeighborList;
fn make_knn(n: usize, k: usize) -> Vec<NeighborList> {
(0..n)
.map(|i| NeighborList {
indices: (0..k).map(|t| ((i + t + 1) % n) as u32).collect(),
distances: vec![1.0; k],
})
.collect()
}
#[test]
fn pair_counts_match_expected() {
let n = 20;
let n_nb = 5;
let n_mn = 2;
let n_fp = 4;
let data: Vec<f32> = (0..n * 3).map(|i| i as f32).collect();
let knn = make_knn(n, 10);
let pairs = build_pairs(&knn, &data, n, 3, n_nb, n_mn, n_fp, Some(0)).unwrap();
assert_eq!(pairs.near.len(), n * n_nb);
assert_eq!(pairs.mid_near.len(), n * n_mn);
assert_eq!(pairs.further.len(), n * n_fp);
}
#[test]
fn near_pairs_no_self_loops() {
let n = 20;
let data: Vec<f32> = (0..n * 3).map(|i| i as f32).collect();
let knn = make_knn(n, 10);
let pairs = build_pairs(&knn, &data, n, 3, 5, 2, 4, Some(0)).unwrap();
for p in &pairs.near {
assert_ne!(p[0], p[1], "near pair should not be a self-loop");
}
}
}