use super::*;
use std::collections::BTreeMap;
const PARALLEL_THRESHOLD: usize = 2048;
const MAX_NEIGHBOR_THREADS: usize = 8;
pub(crate) struct NeighborIndex<'a> {
particles: &'a [Particle],
radius: f64,
cells: BTreeMap<(i64, i64), Vec<usize>>,
coordinates: Vec<(i64, i64)>,
}
impl<'a> NeighborIndex<'a> {
pub(crate) fn new(particles: &'a [Particle], radius: f64) -> Result<Self, ValidationError> {
if !radius.is_finite() || radius <= 0.0 || particles.len() > 65536 {
return Err(invalid(
"neighbors",
"Require a finite positive radius and at most 65536 particles",
));
}
let mut cells = BTreeMap::<(i64, i64), Vec<usize>>::new();
let mut coordinates = Vec::new();
coordinates
.try_reserve_exact(particles.len())
.map_err(|_| limit("Neighbor allocation failed"))?;
for (index, particle) in particles.iter().enumerate() {
particle.validate()?;
let cell = particle
.position
.map(|value| libm::floor(f64::from(value) / radius));
if cell
.iter()
.any(|value| !value.is_finite() || value.abs() > i64::MAX as f64 * 0.5)
{
return Err(invalid(
"neighbors",
"Spatial cell coordinates exceed integer range",
));
}
let cell = (cell[0] as i64, cell[1] as i64);
coordinates.push(cell);
cells.entry(cell).or_default().push(index);
}
Ok(Self {
particles,
radius,
cells,
coordinates,
})
}
pub(crate) fn len(&self) -> usize {
self.particles.len()
}
pub(crate) fn candidates(&self, index: usize) -> impl Iterator<Item = usize> + '_ {
let (horizontal, vertical) = self.coordinates[index];
(vertical - 1..=vertical + 1)
.flat_map(move |row| (horizontal - 1..=horizontal + 1).map(move |column| (column, row)))
.filter_map(|cell| self.cells.get(&cell))
.flatten()
.copied()
.filter(move |other| *other != index)
}
pub(crate) fn within(
&self,
index: usize,
later_only: bool,
work: &mut u64,
max_work: u64,
) -> Result<Vec<usize>, ValidationError> {
let particle = &self.particles[index];
let mut neighbors = Vec::new();
for other in self.candidates(index) {
if later_only && other < index {
continue;
}
*work = work
.checked_add(1)
.ok_or_else(|| limit("Neighbor work overflow"))?;
if *work > max_work {
return Err(limit("Neighbor work allowance exhausted"));
}
if libm::hypot(
f64::from(particle.position[0] - self.particles[other].position[0]),
f64::from(particle.position[1] - self.particles[other].position[1]),
) <= self.radius
{
neighbors.push(other);
}
}
neighbors.sort_unstable();
Ok(neighbors)
}
fn chunk_pairs(
&self,
range: std::ops::Range<usize>,
max_work: u64,
) -> Result<(Vec<(usize, usize)>, u64), ValidationError> {
let mut output = Vec::new();
let mut work = 0;
for index in range {
let neighbors = self.within(index, true, &mut work, max_work)?;
if output.len() + neighbors.len() > MAX_INSTANCE_COUNT {
return Err(limit("Neighbor pair allowance exhausted"));
}
output
.try_reserve(neighbors.len())
.map_err(|_| limit("Neighbor allocation failed"))?;
output.extend(neighbors.into_iter().map(|other| (index, other)));
}
Ok((output, work))
}
pub(crate) fn pairs(&self, max_work: u64) -> Result<Vec<(usize, usize)>, ValidationError> {
let count = self.len();
let base = count as u64;
if base > max_work {
return Err(limit("Neighbor preparation work allowance exhausted"));
}
let remaining = max_work - base;
let threads = if count < PARALLEL_THRESHOLD {
1
} else {
std::thread::available_parallelism()
.map_or(1, usize::from)
.min(MAX_NEIGHBOR_THREADS)
};
let chunks = if threads == 1 {
vec![self.chunk_pairs(0..count, remaining)?]
} else {
let size = count.div_ceil(threads);
std::thread::scope(|scope| {
let handles = (0..count)
.step_by(size)
.map(|start| {
scope.spawn(move || {
self.chunk_pairs(start..(start + size).min(count), remaining)
})
})
.collect::<Vec<_>>();
handles
.into_iter()
.map(|handle| {
handle
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
.collect::<Result<Vec<_>, _>>()
})?
};
let work = chunks
.iter()
.try_fold(0u64, |total, (_, work)| total.checked_add(*work))
.ok_or_else(|| limit("Neighbor work overflow"))?;
if work > remaining {
return Err(limit("Neighbor work allowance exhausted"));
}
let total = chunks.iter().map(|(pairs, _)| pairs.len()).sum::<usize>();
if total > MAX_INSTANCE_COUNT {
return Err(limit("Neighbor pair allowance exhausted"));
}
let mut output = Vec::new();
output
.try_reserve_exact(total)
.map_err(|_| limit("Neighbor allocation failed"))?;
for (pairs, _) in chunks {
output.extend(pairs);
}
Ok(output)
}
}
pub(crate) fn pairs(
particles: &[Particle],
radius: f64,
max_work: u64,
) -> Result<Vec<(usize, usize)>, ValidationError> {
NeighborIndex::new(particles, radius)?.pairs(max_work)
}